Tutorial12 min read · Updated September 10, 2026

How to Add shadcn/ui to Next.js (2026 Guide)

shadcn/ui is not a component library you install — it is a CLI that copies component source code into your repository, where you own it and edit it like any other file. That one decision changes how you use it: there is no version to upgrade, no wrapper to fight, and no theme API to learn beyond CSS variables. This guide covers the setup, the theme, the parts people get wrong, and what the registry system is for.

What shadcn/ui actually is

There is no `npm i shadcn-ui` that gives you components, and that is deliberate rather than an oversight. You run a CLI, it writes `.tsx` files into `src/components/ui/`, and from that moment the code is yours — in your repository, in your diffs, editable without a wrapper or an override API. The project describes itself as a distribution system for components rather than a library, and taking that literally is the key to using it well.

Each component is three well-chosen pieces glued together. Radix UI supplies the unstyled primitive that handles the genuinely hard parts — focus management, keyboard interaction, portalling, ARIA wiring. Tailwind supplies the styling. Class Variance Authority supplies the variant API, so `<Button variant="outline" size="sm">` maps to a class string through a typed lookup rather than a chain of ternaries.

The trade-off is real and worth stating plainly. You get complete control and no dependency on someone else's design decisions; you also get no automatic updates, because upstream fixes do not flow into files you now own. For a product with its own design language this is the right side of the trade. For an internal tool where nobody will ever touch the styling, a conventional library you can upgrade with one command may serve you better.

It also assumes Tailwind. If your project does not use Tailwind, shadcn/ui is not an option — the components are Tailwind class strings, and stripping that out leaves you with Radix primitives, which you could have installed directly. If you are still deciding what the interface needs to contain, write the build prompt first: the screens and states you list there tell you which components you actually need.

Step 1 — Initialise it

Start from a Next.js project that already has Tailwind working. Then run `npx shadcn@latest init`. The CLI asks a short series of questions, installs the dependencies it needs (`radix-ui`, `class-variance-authority`, `clsx`, `tailwind-merge`, `lucide-react`), writes a `components.json`, adds the theme variables to your stylesheet, and creates `src/lib/utils.ts` with the `cn()` helper.

`components.json` is the CLI's memory, and three of its fields cannot be changed afterwards without regenerating everything: `style` (only `new-york` now — `default` is deprecated), `tailwind.baseColor` (the neutral ramp your greys come from: `neutral`, `stone`, `zinc`, `mauve`, `olive`, `mist` or `taupe`), and `tailwind.cssVariables`. Decide those deliberately at init rather than pressing return four times.

Set `rsc: true` for the App Router. It tells the CLI to add the `"use client"` directive to components that need it, which is most of the interactive ones and none of the presentational ones. On Tailwind v4, leave `tailwind.config` as an empty string — there is no JavaScript config file for it to point at, and a stale path there produces a confusing error on the next `add`.

The `aliases` block has to match your `tsconfig.json` paths. If `@/*` maps to `./src/*` there, then `aliases.ui` of `@/components/ui` writes to `src/components/ui`. When the CLI drops files somewhere unexpected, this mismatch is the reason roughly every time.

Step 2 — Add components

`npx shadcn@latest add button` writes `src/components/ui/button.tsx` and installs whatever that component depends on. Pass several at once — `add button card dialog input` — and the CLI resolves shared dependencies rather than installing them repeatedly. Import it as `import { Button } from "@/components/ui/button"` and use it: `<Button variant="outline">Click me</Button>`.

Four flags earn their keep. `--dry-run` prints what would be written without writing it. `--view` shows the component source in the terminal so you can read it before it lands in your tree. `--diff` shows what has changed upstream relative to the copy you already have — which is how you pick up an accessibility fix without blindly overwriting your own edits. `--overwrite` replaces the local file, and should never be run without checking `--diff` first.

Resist `add --all`. It is fast and it leaves you with fifty component files, most unused, all of which a future reader has to assume are load-bearing. Add components when a screen needs them; the CLI takes moments and the repository stays legible.

Everything the CLI writes is ordinary source. Rename the file, move it out of `ui/`, split it into two, delete the variants you do not use — nothing downstream depends on the shape it arrived in. The only cost of deviating is that `--diff` gets less useful for that file, which is a fair price for a component you have genuinely made your own.

Step 3 — Theming

Theming is CSS variables, not a configuration object. `init` writes a set of semantic token pairs into your stylesheet: `background`/`foreground`, `card`/`card-foreground`, `popover`/`popover-foreground`, `primary`/`primary-foreground`, `secondary`, `muted`, `accent`, `destructive`, plus `border`, `input`, `ring`, a five-colour `chart-1`…`chart-5` palette and the `sidebar-*` family. They are defined once under `:root` and again under `.dark`.

Those variables become Tailwind utilities through a `@theme inline` block — `--color-background: var(--background)` and so on — which is why `bg-background` and `text-muted-foreground` exist at all. The `inline` part matters here: it substitutes the reference at the point of use, so redefining `--background` under `.dark` changes what `bg-background` renders without any `dark:` variant in the component.

To rebrand, change the values, not the component files. Set `--primary` to your brand colour under `:root` and to its dark-mode counterpart under `.dark`, and every button, focus ring and active state follows. Use `oklch()` for the values — it is what the generated theme uses, and it keeps a ramp perceptually even. Generate the ramp from your brand colour rather than hand-picking eleven shades that drift in hue.

Adding a token takes three lines and is the correct way to introduce, say, a warning colour. Declare `--warning` and `--warning-foreground` under `:root` and `.dark`, expose them with `--color-warning: var(--warning)` inside the `@theme inline` block, and `bg-warning text-warning-foreground` works everywhere. Do not reach for an arbitrary value like `bg-[#f59e0b]` — it will be wrong in one of the two themes, and it will not be findable when the brand changes.

Step 4 — Customising a component properly

Open `button.tsx` and you will find a `cva()` call listing `variant` and `size` maps. Adding a variant means adding a key — a `brand` entry alongside `default` and `outline` — and it is typed immediately, so `<Button variant="brand">` compiles and `<Button variant="brnad">` does not. This is the intended extension point and it is better than every alternative, including wrapping the component in another component.

For one-off adjustments, pass `className`. The `cn()` helper in `@/lib/utils` runs `clsx` output through `tailwind-merge`, which resolves conflicts by specificity of intent rather than source order — so a caller passing `px-8` genuinely overrides the variant's built-in `px-4` instead of losing to it at random. This is the entire reason `cn()` exists rather than a bare template string.

Use `asChild` when the element needs to be something other than what the component renders. `<Button asChild><Link href="/pricing">Pricing</Link></Button>` renders a single anchor carrying the button's classes — not a link inside a button, which is invalid HTML and breaks keyboard behaviour. It is Radix's Slot pattern, and it appears on most components that render a concrete element.

Resist the urge to restyle from a global stylesheet with `@apply` or descendant selectors. The whole point is that the component's own source is editable; a global override reintroduces the action-at-a-distance problem you avoided by copying the file in. If you find yourself writing `.dialog-content > div { ... }`, open the dialog component instead.

Step 5 — Server and Client Components

In the App Router everything is a Server Component unless marked otherwise, and shadcn components split cleanly along that line. Presentational ones — `Card`, `Badge`, `Separator`, `Skeleton`, and usually `Button` — are plain and render on the server. Interactive ones — `Dialog`, `DropdownMenu`, `Select`, `Popover`, `Sheet`, `Tabs`, anything built on Radix state — carry `"use client"` at the top, which the CLI adds for you when `rsc` is `true`.

The rule that catches people is about handlers, not components. A Server Component cannot pass a function across the boundary, so `<Button onClick={...}>` inside a server page fails even though `Button` itself is server-safe. The fix is to extract the interactive piece into its own `"use client"` component and keep the page a Server Component — not to mark the whole page as client, which drags your data fetching into the browser with it.

Keep the boundary as low in the tree as you can. A client island containing one dropdown is cheap; a `"use client"` at the top of a route layout makes every descendant a Client Component and quietly doubles what ships to the browser. When a page feels heavy, the first thing to check is how far up the nearest `"use client"` sits.

Registries and your own components

The CLI is not limited to the official catalogue. Registries are namespaced, so `npx shadcn@latest view @acme/auth` inspects an item from a registry you have configured, and the `registries` block in `components.json` maps a namespace to a URL template containing a `{name}` placeholder, with support for auth headers on private ones.

`npx shadcn@latest search <registry>` lists what a registry offers, with `-q` to filter. This is the fastest way to find out whether the block you are about to build by hand already exists somewhere — dashboards, sidebars, login forms and data tables are all well covered by community registries.

You can publish your own with `npx shadcn@latest build`, which turns a `registry.json` describing your components into the JSON files the CLI consumes. For an organisation with several Next.js apps, this is how a shared component gets distributed without a private npm package: one registry, `add` it wherever you need it, and each app still owns the copy it received.

There is also `npx shadcn@latest migrate`, which handles the mechanical breaking changes for you — including `radix`, which rewrites the old per-primitive `@radix-ui/react-*` imports to the unified `radix-ui` package. Run it on a clean branch and read the diff. If you would rather have the whole application generated with these components already wired together, describe the app and InBuild generates the Next.js code — a paid plan is required to generate.

Common mistakes

Expecting updates. Nothing you ran `add` on will ever change by itself, because the files are yours. Upstream fixes reach you only when you run `add --diff` and choose to take them. Treat it as a scaffold you maintain, and if that is not what you want, this is the wrong tool.

Running `add --overwrite` after editing a component. It does exactly what it says and your changes are gone. Check `--diff` first, every time.

Changing `style`, `baseColor` or `cssVariables` in `components.json` after the fact. Those settings shaped the files that were already written; editing the JSON afterwards changes nothing that exists and makes the next component inconsistent with the rest.

Assuming it works without Tailwind. The components are Tailwind class strings from top to bottom. There is no CSS-in-JS build, no compiled stylesheet to drop in.

Marking a whole route `"use client"` to fix one handler. It works, and it moves the entire subtree — including data fetching that was happily running on the server — into the browser bundle. Extract the island instead.

Fighting `tailwind-merge` with `!important`. If a `className` is not winning, the usual cause is that the conflict is between two utilities `tailwind-merge` does not know are related, such as a custom utility. Add the variant to the `cva()` map instead of escalating specificity.

How to do it

  1. 1

    Get Tailwind working first

    shadcn/ui generates Tailwind class strings, so a working Tailwind setup is a prerequisite, not a step the CLI does for you.

  2. 2

    Run the init command

    npx shadcn@latest init. Answer style, baseColor and cssVariables deliberately — those three cannot be changed later without regenerating.

  3. 3

    Check components.json

    Set rsc: true for the App Router, leave tailwind.config empty on Tailwind v4, and make sure the aliases block matches your tsconfig.json paths.

  4. 4

    Add the components you need

    npx shadcn@latest add button card dialog. Use --view to read the source first and --dry-run to see what would be written. Avoid --all.

  5. 5

    Set your theme

    Edit the CSS variables under :root and .dark. Add new tokens by declaring the variable and exposing it in the @theme inline block.

  6. 6

    Extend through cva, not wrappers

    Add variant keys in the component's cva() call, pass className for one-offs, and use asChild when the rendered element needs to be a link.

  7. 7

    Keep client boundaries low

    Extract interactive pieces into their own "use client" components rather than marking a page or layout as client to accommodate one handler.

Frequently asked questions

How do I update shadcn/ui components?

You do not, automatically — the files are in your repository and no package manager tracks them. Run npx shadcn@latest add <component> --diff to see what has changed upstream, read the diff, and take the parts you want. Overwriting with --overwrite discards any customisation you made, so it is only safe on components you have not touched.

Why is there no npm package with the components in it?

Because the project's premise is ownership. A published package means an override API, a theme object and a version to keep in step; copying the source means you edit the component directly and nothing sits between you and the markup. The cost is that upstream fixes do not arrive on their own, which is the deliberate trade being made.

Can I use shadcn/ui without Tailwind?

No. Every component is styled with Tailwind utility classes, and there is no compiled stylesheet or CSS-in-JS alternative. If you want the accessible behaviour without the styling, install the Radix primitives directly and style them however you like — that is the layer shadcn/ui is built on.

Do I have to add "use client" myself?

Not to the components — with rsc set to true in components.json, the CLI adds the directive to the ones that need it. You do need it on your own components that use hooks or event handlers. The common failure is passing an onClick from a Server Component to a server-safe component like Button: the component is fine, the function is not serializable, and the fix is to extract the interactive part into a client component.

shadcn/ui or a conventional component library?

shadcn/ui when the interface carries your design language and you expect to modify components rather than configure them. A conventional library such as Mantine or MUI when you want a large catalogue that upgrades with one command and a team that should not be editing component internals. The deciding question is whether owning the source is an asset or a maintenance burden for your particular team.

Where should I put my own components?

Keep src/components/ui/ for CLI-managed primitives and put composed, application-specific components somewhere else — src/components/marketing/ or a folder per feature. The separation keeps add --diff meaningful for the generated files and makes it obvious which files a reader can expect to match upstream and which are entirely yours.

Free tools used in this guide

No signup — each runs in the browser or as a single server-side fetch.

Ready to build?

See a real InBuild template rendered live, with the exact exported code, before you pick a plan — no account needed.

Or skip the tutorial: describe it and InBuild generates the Next.js code for you. A paid plan is required to generate.

Build this with InBuild

More Next.js guides