shadcn/ui Guide for Next.js: Build Component Libraries
On this page
shadcn/ui Guide for Next.js: Build Component Libraries
shadcn/ui changed how many teams think about component libraries. Instead of installing a package and importing pre-built components, you copy the source code directly into your project. You own it. You can edit it. There is no black box and no dependency to fight with when you need something slightly different. For Next.js applications, this model is a natural fit, and this guide walks through how to use it well.
What shadcn/ui Actually Is
The most common misconception is that shadcn/ui is a component library like Material UI or Chakra. It is not. It is a collection of reusable components built on top of Radix UI primitives and styled with Tailwind CSS, distributed through a CLI that copies files into your codebase.
The practical consequences of this are significant:
- You own the code. Components live in your repo, usually under
components/ui/. You can rename, refactor, or delete them. - No version lock-in. There is no
shadcnruntime dependency to upgrade. You pull updates when you choose, file by file. - Full customization. Because the source is yours, deep changes are trivial rather than fighting
!importantoverrides or unsupported props.
The trade-off is that you are responsible for maintenance. Updates do not arrive automatically. For most teams, that ownership is worth it.
Setting Up shadcn/ui in Next.js
Start with a Next.js project using the App Router and TypeScript. If you are creating fresh:
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
Then initialize shadcn/ui:
npx shadcn@latest init
The CLI asks a few questions — your base color, whether you use CSS variables for theming, and the location of your global stylesheet. It then writes a components.json file at the project root. This file is the source of truth for how components are generated:
{
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
Note "rsc": true — this tells the CLI to add the "use client" directive to components that need interactivity, which matters in the App Router where components are Server Components by default.
Adding and Organizing Components
Add components individually with the CLI:
npx shadcn@latest add button card dialog input
Each command drops a file into components/ui/. A typical Button uses class-variance-authority (CVA) to manage variants:
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
},
size: { default: "h-10 px-4 py-2", sm: "h-9 px-3" },
},
defaultVariants: { variant: "default", size: "default" },
}
)
The cn helper in lib/utils.ts merges Tailwind classes intelligently using clsx and tailwind-merge, so consumer overrides win over defaults without specificity headaches.
Building Your Own Component Library on Top
The components/ui/ directory holds primitives. The mistake many teams make is scattering business logic directly into pages that consume those primitives. Instead, build a layered structure:
components/ui/— unmodified or lightly modified shadcn primitives (Button, Input, Dialog).components/common/— your composed, opinionated components (aSubmitButtonwith a loading spinner, aConfirmDialog).components/features/— domain-specific components tied to a feature area.
For example, a reusable confirmation dialog composed from primitives:
"use client"
import { Button } from "@/components/ui/button"
import {
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
} from "@/components/ui/dialog"
export function ConfirmDialog({ open, onConfirm, onCancel, title }: {
open: boolean; onConfirm: () => void; onCancel: () => void; title: string
}) {
return (
<Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
<DialogContent>
<DialogHeader><DialogTitle>{title}</DialogTitle></DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>Cancel</Button>
<Button onClick={onConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
This keeps your primitives pristine and your composed components reusable across the app.
Theming and Design Tokens
shadcn/ui uses CSS variables defined in globals.css, split into :root and .dark blocks. This is where your design system lives:
:root {
--background: /* light background HSL channels */;
--foreground: /* dark foreground HSL channels */;
--primary: /* primary color HSL channels */;
--radius: 0.5rem;
}
.dark {
--background: /* dark background HSL channels */;
--foreground: /* light foreground HSL channels */;
}
Because values are HSL channels without the hsl() wrapper, Tailwind can apply opacity modifiers like bg-primary/90. To rebrand the entire app, change these variables in one place rather than editing every component. Pair this with next-themes for dark mode:
npm install next-themes
Wrap your root layout in a ThemeProvider with attribute="class" so toggling switches the .dark class on <html>.
Server Components and the App Router
In Next.js App Router, prefer keeping interactivity at the leaves. A shadcn Card is a pure presentational component and can render on the server. Only components using hooks, event handlers, or Radix state (Dialog, Dropdown, Popover) need "use client".
A good pattern: fetch data in a Server Component page, pass it to server-rendered presentational shadcn components, and isolate the interactive island (the button that opens a modal) in its own client component. This minimizes the JavaScript shipped to the browser.
Keeping Components Updated
Since you own the code, updates are manual. The CLI can show diffs:
npx shadcn@latest diff button
Best practices for maintainability:
- Commit primitives separately from your customizations so diffs stay readable.
- Avoid heavy edits to
components/ui/files. Wrap and compose instead, so re-pulling an updated primitive does not clobber your logic. - Document intentional deviations with a code comment when you must modify a primitive directly.
Common Pitfalls
- Forgetting
"use client"on interactive components leads to cryptic "hooks can only be used in Client Components" errors. - Hardcoding colors instead of using CSS variables defeats the theming system.
- Treating
components/ui/as untouchable or as a dumping ground — find the middle path of light edits plus composition layers. - Mismatched Tailwind config where content paths do not include
components/, causing styles to be purged in production.
FAQ
Is shadcn/ui free for commercial use? Yes. It is MIT licensed. Because the code lives in your project, there is no licensing runtime or attribution requirement beyond respecting the open-source license.
Does shadcn/ui work with the Pages Router?
Yes. It works with both the App and Pages Routers, and with Vite, Remix, and Astro. The "rsc" flag in components.json simply controls whether the "use client" directive is auto-added.
Do I need Tailwind CSS? Yes. shadcn/ui is built on Tailwind and CSS variables. If your project cannot use Tailwind, this library is not a fit.
How is this different from a normal npm component library? A traditional library ships compiled components you import and configure through props. shadcn/ui copies editable source into your repo, giving you full control at the cost of manual updates.
Can I use it in a monorepo?
Yes. Configure components.json and path aliases per package, or centralize primitives in a shared UI package and re-export them. The CLI supports workspace setups.
How do I handle updates without losing my changes?
Keep primitives close to their original form and layer customizations in separate composed components. Use npx shadcn@latest diff to review upstream changes before applying them.
Conclusion
shadcn/ui pairs naturally with Next.js because both favor ownership and explicitness over hidden abstraction. Start with the CLI-generated primitives, keep them clean, and build your real component library as composition layers on top. Theme through CSS variables, respect the Server/Client Component boundary, and treat updates as deliberate merges rather than automatic upgrades. The result is a component system you fully understand and can evolve with your product — which, for long-lived applications, is exactly what you want.
Sources
Related Articles
Claude API Guide: Streaming, Tools and System Prompts
How to Set Up AI Code Review in GitHub Actions (2026 Guide)
Wire an AI code reviewer into GitHub Actions the right way — trigger on pull requests, post inline comments, keep secrets safe, and avoid the noisy-bot trap. Complete working workflow included.
AI Code Review Prompts That Actually Work (With Examples)
The quality of an AI code review is decided almost entirely by the prompt. Review prompt patterns that produce signal instead of noise — copy-paste examples for bugs, security, and PR-level review.