Next.js Component Library Setup for the App Router

Most guides to adding a Next.js component library stop at the install command. The questions start right after it: which files need "use client", why a server page cannot pass onClick to a button, why the dark theme flashes on reload, and why a theme toggle does nothing in a fresh app. This guide answers those first, then walks through a full Next.js component library setup with real commands, from create-next-app to updating components months later.
Everything below was run on October 13, 2026 with create-next-app@latest (Next.js 16.4.0, React 19.3.0, Tailwind CSS v4), the wingo-ui CLI 1.0.3 and next-themes 0.4.6. The library is ours, Wingo UI, but Part 1 applies to any source-owned library, including a shadcn Next.js App Router project. For the token side in depth, read the pillar on Tailwind v4 design tokens.
What makes a component library work well in the App Router?
Four properties. If a library has them, the App Router stays out of your way. If it does not, you end up writing wrappers.
"use client" at the file that needs it
In the App Router every file is a server component until it says otherwise. A use client component library marks each interactive file at the top, so the boundary sits inside the library and your pages stay server components. Every Wingo UI file that uses state, effects or context starts with the directive, including Button, Card, Dialog and the UIProvider in lib/ui-config.tsx.
That gives you one rule to remember: a server page can render these components and pass them strings, numbers, objects and JSX, but not functions. <Button onClick={...}> inside a server page fails, because React cannot send a function from the server to the browser. Move the handler into a small client file. Step 6 below shows the pattern.
No window at module scope
Server rendering imports every module on the server, where window, document and matchMedia do not exist. A library that reads them while the module loads, or during render, crashes or renders something different on each side. Ours follows one rule: no window or document at module scope or during render. Browser reads happen in effects or in useSyncExternalStore with a server fallback, so useIsMobile() returns false on the server and updates after hydration. The React use mobile hook guide shows why that matters for layout.
A theme without a flash
The server does not know whether the visitor picked dark mode, because that choice lives in localStorage. The fix is a tiny script that sets the class on <html> before the first paint. next-themes injects that script for you. React then sees an <html> element that differs from the server HTML, which is why the root layout carries suppressHydrationWarning. More on that in step 5, and in our Next.js and Tailwind CSS guides.
Tokens in CSS, not in a JavaScript runtime
Colors, radii, shadows and motion timings are CSS variables in globals.css, mapped to utilities with @theme inline. Dark mode swaps the variables under .dark. Nothing has to run in JavaScript to know what bg-surface means, so server components render the right colors and there is no theme provider to wait for. This is what makes Next.js Tailwind v4 components cheap to theme: you edit one file.
How do I set up a Next.js component library, step by step?
1. Create the app
Skip this in an existing project that already runs Tailwind CSS v4.
On 16.4.0 this writes app/globals.css with @import "tailwindcss";, a tsconfig.json with the @/* alias, and a dark palette inside @media (prefers-color-scheme: dark).
2. Remove the starter's dark media block
This step is easy to miss. When wingo-ui init finds a prefers-color-scheme block, it respects it and puts the library's dark values there too. That is right for an app that only follows the system setting, but next-themes toggles a .dark class, and a media query ignores that class. Delete this block from app/globals.css before you run init:
With the block gone, init writes a .dark block and @custom-variant dark (&:is(.dark *)); instead. We ran init both ways: with the block it reported dark mode: media, without it dark mode: class. The theming docs describe both modes.
3. Run init
The CLI detects the framework from package.json, the @/* alias from tsconfig.json and the stylesheet that imports Tailwind. Then it:
- installs
clsxandtailwind-mergeand createslib/utils.tswith thecnhelper, - merges the design tokens into
app/globals.css, adding only names you do not have yet, so--backgroundand--foregroundfrom the starter stay, - writes
wingo-ui.jsonwith the framework, aliases, stylesheet and package manager.
Our run printed added 72 variables, 46 dark values, 72 @theme mappings, @custom-variant dark, base border color (dark mode: class). With --yes it skips the prompt that offers to connect a coding agent.
4. Add components
Files land in components/ui, hooks and lib. The command pulls what the three items import: the Drawer that Dialog becomes on phones, lib/motion.ts, lib/tones.ts, lib/ui-config.tsx and four hooks. It also installs the npm packages (@radix-ui/react-dialog, motion, vaul, lucide-react and a few more). Free items need no account or key.
wingo-ui.json now records each item with its version and a SHA-256 hash per file, and the CLI keeps the installed copies in .wingo-ui/. Commit both: they are what lets update merge new releases into files you edited.
5. Wrap the root layout
Install next-themes, then replace app/layout.tsx:
The layout stays a server component. ThemeProvider and UIProvider are client components, and the props passed to them are plain data, so this is allowed.
attribute="class"makes next-themes setclass="dark"on<html>, which the.darktokens and thedark:variant follow.suppressHydrationWarningis needed because the theme script changes<html>before React hydrates. It applies to that element's own attributes only, one level deep, so it will not hide real mismatches in your pages.disableTransitionOnChangestops every color transition from animating at once when the theme flips.UIProvidersets app-wide defaults by component name and the locale for dates and numbers. Here every button is fully rounded unless a button setsradiusitself; explicit props always win.
6. Use a client component inside a server page
Pages stay async server components that fetch data. Components with no handlers render straight from the page, and JSX can travel through props, so a whole dialog with a trigger and a footer works without any client file of your own:
The part with state and an onClick goes in its own file:
Both files pass tsc --noEmit and ESLint with the config create-next-app generates. The dialog below is the same component with a form inside. Try it on a narrow screen too.
$ npx wingo-ui@latest add dialog7. Add a theme toggle
Theme Toggle is a Pro component: an animated sun and moon button, a segmented control or a dropdown, all reading next-themes. On the free tier, a short client file does the job:
The icons switch with CSS, so the server HTML and the first client render match and there is no mounted flag to manage. The common causes of the hydration warning are covered in our next-themes hydration error fix, and the variant and tokens behind the toggle in our Tailwind v4 dark mode guide. More posts live in Next.js and Tailwind CSS.
8. Sign in for Pro components
Pro items need a key. Sign the CLI in once per machine:
It opens the browser to approve the device and saves a key under ~/.config/wingo-ui. On a server or in CI, pass a key from your account instead:
Without a key, adding a Pro item stops with a message that names the item and links the plans. The plans are on the pricing page.
9. Keep components up to date
We edited one label in an installed file and ran diff: it printed a unified diff of our change against upstream. update does a three-way merge with the copy saved in .wingo-ui/, keeps your edits, and marks real conflicts like git does. The CLI docs list every flag.
What should I check when something looks wrong?
Where to go next
Start with the free basics: npx wingo-ui@latest add button dialog card, then follow the installation docs for the shadcn CLI and manual paths. If you write UI with Claude Code, Codex or Cursor, connect the MCP server so the agent searches the library before it writes a component. Building on Vite instead of Next.js? The shadcn Vite setup covers the same steps there.
FAQ
Can I use a client component library in Next.js server components?
Yes. A server page can import and render a file marked use client and pass it strings, numbers and JSX. It cannot pass functions such as onClick, so put handlers in a small client component and render that from the page.
Why does the root layout need suppressHydrationWarning?
next-themes runs a script before React hydrates that adds the dark or light class to the html element. The server HTML did not have that class, so React would warn about a mismatch. The attribute silences the warning on that one element only.
Why does my theme toggle not switch colors after wingo-ui init?
The create-next-app stylesheet ships a @media (prefers-color-scheme: dark) block, and init puts the dark values there when it finds one. Delete that block before running init, or move the dark values into a .dark block and add @custom-variant dark (&:is(.dark *)); so the class from next-themes wins.
Do free Wingo UI components need an account or API key?
No. Free items such as button, card and dialog install with npx wingo-ui@latest add and no key. Pro items need npx wingo-ui@latest login once per machine, or WINGO_UI_API_KEY in CI.
Does this setup work with Next.js 15?
We ran every step on Next.js 16.4.0. The library needs React 19 and Tailwind CSS v4, which a Next.js 15 App Router project can also use, but we did not rerun the steps there.
- Next.js
- App Router
- Setup
- Installation
- Tailwind CSS v4


