Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Next.js and Tailwind CSS
  4. Next.js Component Library Setup for the App Router
Auf Deutsch lesen
  1. Blog
  2. Next.js and Tailwind CSS
  3. Next.js Component Library Setup for the App Router
Next.js and Tailwind CSS
Next.js and Tailwind CSS

Next.js Component Library Setup for the App Router

SR
Serban Rusu · Founder of Wingo UI
Oct 13, 2026 · 8 min read

On this page

0%
  1. What makes a component library work well in the App Router?
    1. "use client" at the file that needs it
    2. No window at module scope
    3. A theme without a flash
    4. Tokens in CSS, not in a JavaScript runtime
  2. How do I set up a Next.js component library, step by step?
    1. 1. Create the app
    2. 2. Remove the starter's dark media block
    3. 3. Run init
    4. 4. Add components
    5. 5. Wrap the root layout
    6. 6. Use a client component inside a server page
    7. 7. Add a theme toggle
    8. 8. Sign in for Pro components
    9. 9. Keep components up to date
  3. What should I check when something looks wrong?
  4. Where to go next
  5. FAQ
    1. Can I use a client component library in Next.js server components?
    2. Why does the root layout need suppressHydrationWarning?
    3. Why does my theme toggle not switch colors after wingo-ui init?
    4. Do free Wingo UI components need an account or API key?
    5. Does this setup work with Next.js 15?

TL;DR

A component library fits the Next.js App Router when each interactive file starts with use client, nothing touches window while the module loads, colors live as CSS variables in globals.css and the theme is set before the first paint. In a new Next.js 16 app, remove the starter's prefers-color-scheme block, run npx wingo-ui@latest init, add components, then wrap the root layout in next-themes and UIProvider with suppressHydrationWarning on the html tag. Server pages import the components directly; event handlers move into small client files.

Published Oct 13, 2026

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.

bash
npx create-next-app@latest my-app --ts --tailwind --eslint --app --import-alias "@/*"
cd my-app

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:

css
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

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

bash
npx wingo-ui@latest init

The CLI detects the framework from package.json, the @/* alias from tsconfig.json and the stylesheet that imports Tailwind. Then it:

  • installs clsx and tailwind-merge and creates lib/utils.ts with the cn helper,
  • merges the design tokens into app/globals.css, adding only names you do not have yet, so --background and --foreground from the starter stay,
  • writes wingo-ui.json with 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

bash
npx wingo-ui@latest add button dialog card

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:

bash
npm install next-themes
tsx
// app/layout.tsx
import { ThemeProvider } from "next-themes"
import { UIProvider } from "@/lib/ui-config"
import "./globals.css"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<UIProvider locale="en-US" defaults={{ button: { radius: "full" } }}>
{children}
</UIProvider>
</ThemeProvider>
</body>
</html>
)
}

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 set class="dark" on <html>, which the .dark tokens and the dark: variant follow.
  • suppressHydrationWarning is 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.
  • disableTransitionOnChange stops every color transition from animating at once when the theme flips.
  • UIProvider sets app-wide defaults by component name and the locale for dates and numbers. Here every button is fully rounded unless a button sets radius itself; 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:

tsx
// app/page.tsx
import { Button } from "@/components/ui/button"
import { Dialog, DialogClose } from "@/components/ui/dialog"
import { InviteForm } from "./invite-form"
async function getProject() {
return { name: "Website redesign", members: 4 }
}
export default async function Page() {
const project = await getProject()
return (
<main className="mx-auto flex max-w-xl flex-col gap-6 p-6">
<h1 className="text-xl font-semibold">{project.name}</h1>
<Dialog
trigger={<Button tone="brand">Invite people</Button>}
title="Invite people"
description={`They get access to ${project.name}.`}
footer={
<DialogClose asChild>
<Button variant="soft">Done</Button>
</DialogClose>
}
>
<InviteForm />
</Dialog>
</main>
)
}

The part with state and an onClick goes in its own file:

tsx
// app/invite-form.tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
export function InviteForm() {
const [sent, setSent] = useState(false)
return (
<Button variant="outline" onClick={() => setSent(true)} disabled={sent}>
{sent ? "Invite sent" : "Send invite to emma@example.com"}
</Button>
)
}

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.

Press Edit to open the dialog. Below 768px the same component opens as a bottom sheet you can drag down to close
$ npx wingo-ui@latest add dialog
FreeDialog docs

7. 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:

tsx
// components/theme-switch.tsx
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeSwitch() {
const { resolvedTheme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
aria-label="Toggle theme"
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
>
<Moon className="dark:hidden" />
<Sun className="hidden dark:block" />
</Button>
)
}

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:

bash
npx wingo-ui@latest login

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:

bash
npx wingo-ui@latest login --key wui_...
# or
export WINGO_UI_API_KEY=wui_...

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

bash
npx wingo-ui@latest outdated # what changed upstream
npx wingo-ui@latest diff button # your file against the latest release
npx wingo-ui@latest update # merge new releases into your copies

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?

SymptomLikely causeFix
Theme toggle changes nothingDark values sit in a prefers-color-scheme media blockStep 2, then run init again
"Functions cannot be passed directly to Client Components"onClick passed from a server pageMove the handler into a client file (step 6)
Hydration warning on <html>suppressHydrationWarning missingAdd it to <html> only
Components unstyled, bg-surface does nothingTokens not in the stylesheet Tailwind readsRun npx wingo-ui@latest init again; it adds only missing tokens
Pro item fails to installNo key on this machinenpx wingo-ui@latest login or WINGO_UI_API_KEY

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.

Components in this post

  • Button

    The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps.

    Free
  • Dialog

    A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling.

    Free
  • UIProvider

    A React config provider for Wingo UI: app-wide default props for every component, the Intl locale and a reduced-motion policy.

    Free
  • Theme Toggle

    A React theme toggle for light, dark and system mode on next-themes: an animated sun and moon button, a segmented control or a menu.

    Pro

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

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Next.js and Tailwind CSS

shadcn Vite Setup with Tailwind CSS v4 and React 19

shadcn Vite setup tested on Vite 8 and TypeScript 6: the Tailwind v4 plugin, the @ alias in tsconfig.app.json, dark mode without Next.js, then add components.

SRSerban Rusu·Oct 13, 2026·7 min read
Building UI with AI agents

AGENTS.md vs CLAUDE.md vs Cursor Rules for React

AGENTS.md vs CLAUDE.md vs Cursor rules for React and Tailwind: which agent reads which file in October 2026, and a rules file that keeps agents on your tokens.

SRSerban Rusu·Oct 9, 2026·14 min read
Component guides

How to Update shadcn Components Without Losing Edits

Update shadcn components without losing edits: what shadcn diff and add --diff show, why a 2-way diff hides whose change is whose, and how a 3-way merge works.

SRSerban Rusu·Oct 9, 2026·10 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI