Set up a Next.js App Router or Pages Router project with Proper UI in minutes: providers, dark mode, and Tailwind v4 scanning included.
Proper UI ships as a regular npm workspace package, so a Next.js app consumes it the same way it consumes any other dependency: install it, import its styles, and wrap the tree in two providers.
Installation
The fastest path is the CLI. Run it from the root of your Next.js project:
npx @properui/cli@latest init --nextjs
It detects App Router vs Pages Router and whether you use a src/ directory automatically: --nextjs only overrides framework detection, it doesn't scaffold a new project. The CLI writes app/globals.css (or styles/globals.css for a Pages Router project) with the full stylesheet block inserted after your existing @import "tailwindcss" line (theme import, plugins, custom variants and utilities, not just a bare @source line), sets transpilePackages in next.config.ts, and, unless you pass --no-providers, wires up both providers in your root layout: ThemeProvider (the registry's next-themes-based one, with forcedTheme/enableSystem props available) on every router, and on the App Router, RouterProvider from providers/router-provider.tsx as well, since that file reads next/navigation and works out of the box there. A Pages Router project still needs RouterProvider set up by hand (see below): that provider reads next/router instead, so init can't wire it automatically. init doesn't install any npm packages on its own: it prints the install command last unless you pass --install, and --no-tooling-ignores skips the ESLint/Prettier ignore entries it otherwise adds for the vendored component directories. You're ready to add components once it's done.
Manual installation
Prefer to wire it up yourself, or already have opinions about your layout.tsx? Here's every step the CLI performs.
1. Install the package
npm install @properui/ui react-aria-components next-themes
2. Import the stylesheet
@properui/ui publishes a single globals.css that already contains the Tailwind import, the design tokens, typography, and every plugin the components need. Import it once and add an @source line so Tailwind v4 scans the package's own class names:
/* app/globals.css */
@import "@properui/ui/styles/globals.css";
@source "../node_modules/@properui/ui/src/**/*.{ts,tsx}";
3. Transpile the package
Next.js only compiles your own source by default. withProperUI is the one-liner: it appends @properui/ui to transpilePackages, deduplicated, and leaves the rest of your config untouched.
// next.config.ts
import type { NextConfig } from "next";
import { withProperUI } from "@properui/ui/next";
const nextConfig: NextConfig = {};
export default withProperUI(nextConfig);
Equivalent by hand, so its TSX is processed the same way your app code is:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@properui/ui"],
};
export default nextConfig;
4. Load a font with next/font
Proper UI's type scale reads from a --font-body (and --font-display) token in theme.css. Point them at whatever font next/font generates instead of loading a stylesheet:
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
// ...then: <html lang="en" suppressHydrationWarning className={inter.variable}>
/* app/globals.css, after the imports above */
@theme {
--font-body: var(--font-inter), system-ui, sans-serif;
--font-display: var(--font-inter), system-ui, sans-serif;
}
App router vs pages router
Both routers need the same two providers, but they're not interchangeable. ThemeProvider (from @properui/ui/providers, wrapping next-themes) works identically in both. RouterProvider (from @properui/ui/providers/router-provider, its own subpath so the shared providers barrel stays free of Next imports) does not take a navigate prop: it reads useRouter from next/navigation internally, which only exists in the App Router. A Pages Router app has to use React Aria's own RouterProvider directly, fed from next/router.
App router
Both provider files already carry their own "use client" directive, so you can render them straight from the (server) root layout with no wrapper file of your own:
// app/layout.tsx
import { ThemeProvider } from "@properui/ui/providers";
import { RouterProvider } from "@properui/ui/providers/router-provider";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="bg-primary text-primary antialiased">
<ThemeProvider>
<RouterProvider>{children}</RouterProvider>
</ThemeProvider>
</body>
</html>
);
}
suppressHydrationWarning on <html> is required: next-themes writes the theme class before React hydrates, and without it React reports a mismatch. ThemeProvider forwards every next-themes prop, so defaultTheme, enableSystem, storageKey, and friends all still work if you want to override its defaults.
Pages router
The bundled RouterProvider doesn't work here; swap it for React Aria's own, fed next/router's useRouter:
// pages/_app.tsx
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { RouterProvider } from "react-aria-components";
import { ThemeProvider } from "@properui/ui/providers";
import "../styles/globals.css";
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
return (
<ThemeProvider>
<RouterProvider navigate={router.push}>
<Component {...pageProps} />
</RouterProvider>
</ThemeProvider>
);
}
Using Proper UI components
Import each component from its own subpath; the same path the CLI writes when you run add:
import { Button } from "@properui/ui/components/base/buttons/button";
export default function Page() {
return <Button size="md">Get started</Button>;
}
Most base components already carry their own "use client" directive because they're built on React Aria hooks, so you can import them directly into a server component's JSX without adding the directive yourself: the boundary is drawn inside the component, not at the call site. You only need "use client" on your own file when you add interactivity (a click handler, useState, a form) around the components you're composing.
Icon props, from a server component specifically. The usual form passes the icon component itself, <Button iconLeading={ArrowRight}>, and the component applies its own sizing and data-icon attribute. That form doesn't survive being rendered straight from a true server component (a file with no "use client" of its own that renders Button directly): a bare component reference can't cross the server/client boundary, and the build fails, sometimes with an error pointing at an unrelated page. Pass an element instead, and set data-icon yourself since the wrapper never runs:
<Button iconTrailing={<ArrowRight data-icon="trailing" />}>Continue</Button>
Use data-icon="leading" for iconLeading. This only matters inside a real server component; once you're inside a "use client" file, the plain component-reference form works as documented everywhere else.
Dark mode
Dark mode is class-based: Proper UI looks for .dark-mode on <html> and flips every semantic token underneath it. The value map passed to ThemeProvider above tells next-themes to write light-mode / dark-mode instead of its default light / dark class names, and suppressHydrationWarning on <html> stops React from complaining about the class next-themes adds before hydration.
Theme toggle
next-themes exposes the current theme through useTheme. Wire it to a button and you have a working toggle:
// components/theme-toggle-button.tsx
"use client";
import { useTheme } from "next-themes";
import { Button } from "@properui/ui/components/base/buttons/button";
export const ThemeToggleButton = () => {
const { resolvedTheme, setTheme } = useTheme();
return <Button onPress={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>Toggle theme</Button>;
};
If your tool supports MCP servers, the Proper UI MCP lets the agent search screens, flows and sections and plan the install, with the CLI writing the files.