How to Use shadcn/ui: The Beginner's Guide (Current CLI Setup, First Components, Theming, and the Copy-Paste Model)
CodexFuse Team · 2026-10-31T03:57:27.257Z · 14 min read
What is shadcn/ui, exactly?
shadcn/ui is a collection of beautifully designed, accessible React components that you copy into your own project instead of installing as a package. That single difference is the whole idea: there is no shadcn npm dependency to update, no version lock, and no fighting a library's theme object — the component source code lives in your components/ui folder, and you own it completely. This is how to use shadcn in the current era: run the shadcn@latest CLI to initialize your project, add the components you need one at a time, style them with Tailwind CSS, and edit any component's source when your design needs something different.
Under the hood, shadcn/ui components are built on three things: Radix UI primitives (the accessible, unstyled building blocks for dialogs, menus, and popovers), Tailwind CSS (all the styling), and a small variant system powered by class-variance-authority.
When you run pnpm dlx shadcn@latest add button, the CLI copies the Button's source into your project and wires up its dependencies. From that moment the Button is yours — change its colors, its radius, its behavior, anything.
Because theming runs through Tailwind and CSS variables, picking your base color early matters, and our Tailwind CSS color palette tool is a handy way to preview palettes before you commit to one in the init wizard.
What do you need before you start?
You need three things before the CLI will do anything useful: Node.js installed, a React project, and a code editor. The CLI works with Next.js, Vite, React Router, Astro, TanStack Start, and Laravel projects, so you are not locked into one framework — but this guide follows the Next.js path because it is the most documented. If you are starting completely from zero, read our VS Code beginner guide first; you will live in your editor for this whole process.
You also need a working familiarity with Tailwind CSS utility classes. shadcn/ui does not hide Tailwind from you — every component is styled with classes like bg-primary and text-muted-foreground right in the source you own. If flex, gap-4, and rounded-md already feel natural, you are ready. If not, spend an hour with Tailwind basics first; this guide will still be here.
How do you set up shadcn/ui with the current CLI?
You set up shadcn/ui by running shadcn@latest init — but the exact command depends on where you are starting from, so pick your path from this table first.
| Your starting point | Command to run | What happens |
|---|---|---|
| Brand-new project, terminal-first | pnpm dlx shadcn@latest init -t next | Scaffolds a fresh Next.js app with shadcn/ui wired in; templates also exist for vite, start, react-router, and astro |
| Brand-new project, visual setup | Build a preset at shadcn/create, then run the generated command | You pick style, colors, fonts, and icons in a UI; the site generates a command like pnpm dlx shadcn@latest init --preset [CODE] --template next |
| Existing Next.js / Vite project | pnpm dlx shadcn@latest init | The wizard asks about base color, preset, and monorepo setup, then writes components.json and your theme CSS |
| Laravel project | laravel new first, then npx shadcn@latest init | Laravel needs its app scaffolded before the shadcn init step |
Use whichever package runner matches your project: npx shadcn@latest, pnpm dlx shadcn@latest, or bunx --bun shadcn@latest. One critical detail for this shadcn/ui tutorial: the package name is shadcn@latest. The old shadcn-ui@latest name you will see in older blog posts is retired — if a guide tells you to install that, the guide is out of date.
For the existing-project path, which is what most beginners actually do, the steps are:
- Create the project if you do not have one:
pnpm create next-app@latest, choosing the recommended defaults so Tailwind CSS, the App Router, and the@/*import alias are configured for you. - Make sure
tsconfig.jsonmaps the@/*alias (to./*, or./src/*if you used--src-dir). - Run
pnpm dlx shadcn@latest initand answer the wizard: pick a style, a base color (Neutral, Stone, Zinc, Mauve, Olive, Mist, or Taupe), and whether to use CSS variables (yes — it is the default and the theming story below assumes it). - Check that
components.jsonwas created at your project root — it records your style, your Tailwind CSS path, and your base color so every futureaddcommand stays consistent.
How do you add your first components?
You add components with the add command, one at a time, exactly when you need them. Run pnpm dlx shadcn@latest add button and the CLI copies button.tsx into your project's components/ui folder (or src/components/ui if you use a src directory), installing any missing dependencies like class-variance-authority along the way. Then you import and use it like any component you wrote yourself:
import { Button } from "@/components/ui/button"
export default function Home() {
return (
<div className="flex min-h-svh items-center justify-center">
<Button>Click me</Button>
</div>
)
}
You can add several components in one command — pnpm dlx shadcn@latest add button card input label — or add every available component with pnpm dlx shadcn@latest add --all. Beginners should resist the --all urge: adding components as you need them keeps your project small and teaches you what each one actually does. For this guide, add button, input, label, card, dialog, and form — those six build the real form page later.
Beyond single components, the CLI can add whole blocks — pre-built compositions like login forms. pnpm dlx shadcn@latest add login-03 drops a complete login block into your project, which you then import from @/components/login-03. Blocks are the fastest way to see how shadcn components compose in a real layout, and they are fully editable like everything else.
How do Button variants work?
Button variants work through class-variance-authority (cva): the generated button.tsx defines a buttonVariants function with named variants and sizes, and you pick one per usage with props. The standard variant set is default, destructive, outline, secondary, ghost, and link; the size set is sm, default, lg, and icon. Using them looks like this:
import { Button } from "@/components/ui/button"
<Button variant="destructive">Delete account</Button>
<Button variant="outline" size="sm">Cancel</Button>
<Button variant="ghost" size="icon">...</Button>
The rule the ecosystem follows is: reach for a built-in variant before writing custom styles. Nine times out of ten the look you want already exists as a variant, and using it keeps your UI consistent for free. If you are prototyping the visual design of a button before committing it to a component, our CSS button generator post walks through building button styles interactively — a useful sketchpad before you encode the final look as a variant.
Because the variant definitions live in your own button.tsx, adding a brand variant is a small, safe edit: add an entry like brand: "bg-orange-600 text-white hover:bg-orange-700" to the variants object, and <Button variant="brand"> works everywhere instantly. No theme override files, no !important wars — just source code you can read.
What is the cn() utility and why is it everywhere?
cn() is the tiny class-merging helper the CLI writes into lib/utils.ts (or src/lib/utils.ts), and every shadcn component uses it to combine classes safely. Its whole implementation is two lines: it runs its inputs through clsx (which handles conditionals, arrays, and objects) and then through tailwind-merge (which resolves conflicting Tailwind classes intelligently):
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
The tailwind-merge half is the part that matters. Without it, writing conditional padding by hand leaves both px-4 and px-8 in the output, and the winner depends on CSS order — a classic Tailwind bug.
cn("px-4", wide && "px-8") correctly keeps only px-8 when wide is true. Components use cn() internally so that any className you pass when using the component merges cleanly with the component's own classes instead of fighting them.
How does theming work with Tailwind v4?
Theming works through CSS variables mapped into Tailwind utilities — the current, Tailwind v4-native way, not the old tailwind.config.js file. When you run init with CSS variables enabled (the default), the CLI writes theme tokens into your global CSS file, typically app/globals.css. The file starts with two imports and a dark-mode variant declaration:
@import "tailwindcss";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
Then an @theme inline block maps each semantic token — --background, --foreground, --primary, --muted, --border, --ring, and friends — to Tailwind color utilities, so components can use classes like bg-background, text-foreground, and border-border. The actual color values live in :root for light mode and are overridden inside a .dark selector for dark mode, all in OKLCH. The base color you picked in the wizard (Neutral, Stone, Zinc, Mauve, Olive, Mist, or Taupe) simply decides which default token values get generated.
The token system is semantic, not literal: primary means "high-emphasis actions and brand surfaces" and pairs with primary-foreground for the text that sits on it; muted means subtle surfaces with muted-foreground for helper text; destructive marks dangerous actions. Changing your brand color later means editing a handful of token values in one CSS file — every Button, Badge, and Card updates at once because they all reference tokens, never raw hex values. If you are choosing those token colors, check your text-on-background pairs with a contrast token generator so body text stays readable in both modes.
Two more theming facts beginners should know. First, --radius (default 0.625rem) is the single source of truth for corner roundness: the theme derives radius-sm through radius-4xl from it with calc(), so changing one value reshapes every component.
Second, you can add your own tokens — define --warning and --warning-foreground under both :root and .dark, expose them with --color-warning entries inside @theme inline, and bg-warning becomes a real utility.
If you would rather skip CSS variables entirely, pnpm dlx shadcn@latest init --no-css-variables generates components with inline Tailwind color utilities instead — but that is an installation-time choice; switching later means deleting and re-adding your components.
How do you add dark mode?
You add dark mode by overriding the same theme tokens inside a .dark selector and toggling that class on your page. The theme scaffold the CLI generates already includes the full .dark block — dark backgrounds, flipped foregrounds, dimmed borders — so in a default setup dark mode is mostly a matter of turning it on, not designing it. The @custom-variant dark (&:is(.dark *)) line in your CSS tells Tailwind v4 that dark: prefixed utilities should respond to the .dark class on any ancestor.
In practice, most projects add a small theme provider that toggles the class and remembers the user's choice, plus a toggle button (often the Sun/Moon icon button in the header). The official dark-mode docs walk through the provider setup step by step. Because dark mode is just token overrides, any custom token you add must have both a :root value and a .dark value — a token with only a light value will look broken the moment someone flips the switch, and that is the single most common dark-mode bug in shadcn projects.
How do you edit a component you added?
You edit it like any file you wrote: open components/ui/button.tsx and change it. This is the copy-paste model's whole payoff and the thing beginners from the npm-package world find hardest to believe. There is no node_modules copy to fight, no peer-dependency warning when you diverge, and no upstream changelog to watch — the file is yours, and your edits are the API.
Three edits cover most real-world needs: changing a variant's classes in the cva definition, adjusting the base classes every instance shares, or restructuring the JSX (adding an icon slot, wrapping the trigger, changing the underlying Radix primitive's props). Keep the component's prop interface stable while you restyle and every usage keeps working. The one discipline this freedom requires: if you heavily customize a component, note what you changed, because re-running add for that component later will overwrite your edits with the fresh copy.
How do you build a real form page with four components?
You build it by composing Card, Form (or plain Input + Label), and Button — this is the moment the pieces click together. First make sure the components are in your project: pnpm dlx shadcn@latest add card form input label button. The Form component wraps React Hook Form with accessible labels and error messages, so a newsletter signup page looks like this:
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function NewsletterPage() {
return (
<div className="flex min-h-svh items-center justify-center">
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Stay in the loop</CardTitle>
<CardDescription>One email a month. No spam, ever.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="[email protected]" />
</div>
<Button className="w-full">Subscribe</Button>
</CardContent>
</Card>
</div>
)
}
Notice the composition pattern: Card is never used bare — it is always CardHeader + CardTitle + CardDescription + CardContent (+ CardFooter when you need one). The same nesting discipline applies across the library: SelectItem always lives inside a SelectGroup, DialogContent pairs with DialogHeader/DialogTitle, and so on. Learning these small composition rules is what makes shadcn code read cleanly — the components are designed to be assembled, not used one at a time.
Where do components actually come from?
Components come from registries, and the default registry is the official one maintained with the shadcn/ui docs. But the CLI's registry model is open: a growing ecosystem of community registries publishes their own components, and you can install from any registry URL with npx shadcn@latest add @[registry]/[name]. There is a public registry directory for browsing them.
This is also where the project's GitHub home matters — the component source, the CLI, and the docs all live in the open, so when something behaves oddly you can read the actual implementation instead of guessing. Our GitHub beginner guide covers the clone-and-read workflow if you have never dug through a real repo before.
One naming trap to avoid: shadcn-vue is the parallel port of the same idea for Vue — same copy-paste philosophy, same Tailwind styling, but a different codebase for a different framework. This guide, and the shadcn@latest CLI, are the React side.
If you are building with Vue or Nuxt, reach for shadcn-vue deliberately, not by accident.
shadcn/ui vs MUI: which should a beginner pick?
A beginner who wants full design control with minimum fighting should pick shadcn/ui; a beginner who wants a batteries-included component API with theming done through a config object should pick MUI. The difference is architectural, not cosmetic.
MUI (Material UI) is a traditional npm package: you npm install it, it ships its own styles and version number, and you customize through its theme provider and sx prop. It is polished, huge, and opinionated — Material Design looks like Material Design unless you work to change it.
shadcn/ui inverts the trade: there is no package and no version to bump, the styles are plain Tailwind classes you can read, and the design is neutral enough to become anything. The price is responsibility — you own updates (re-run add to refresh a component) and you own the design system.
For a beginner building a personal or startup project with Tailwind already in the stack, shadcn/ui teaches more and constrains less. For a beginner on a team that needs a governed design system with long-term support guarantees, MUI's package model is the safer corporate choice.
What mistakes do beginners make with shadcn/ui?
The most expensive mistake is following an outdated guide that teaches the retired shadcn-ui@latest CLI or a tailwind.config.js-based setup. The current CLI is shadcn@latest and the current styling path is Tailwind v4 CSS-first (@import "tailwindcss" plus CSS variables) — if a tutorial asks you config-file questions the current wizard never asks, close the tab. The field is full of pre-v4 guides because the ecosystem moved fast; always check the date and the package name.
The second mistake is treating shadcn/ui like an npm package: running npm install shadcn (there is no such package to install), or expecting components to update themselves. Nothing updates until you re-run add, and that is a feature — your UI never changes under you because a maintainer shipped a new minor version overnight.
The third mistake is editing with fear: beginners tiptoe around the generated files as if they were library code. They are not. The whole point is that components/ui is your code — read it, change it, break it, learn from it.
Frequently asked questions
Is shadcn/ui free to use?
Yes. The components, the CLI, and the registries are free and open source. There is no paid tier, no license key, and no per-seat cost — you copy the code into your project and it is yours.
Do I need Next.js to use shadcn/ui?
No. The CLI scaffolds new projects for Next.js, Vite, TanStack Start, React Router, and Astro templates, and the init command works in existing projects on those frameworks (plus Laravel after laravel new). Next.js is simply the best-documented path.
How is shadcn/ui different from MUI?
MUI is an npm package you install and theme through its config; shadcn/ui is source code the CLI copies into your project that you own and edit directly. MUI gives you a governed, versioned library, while shadcn/ui gives you full control with no version lock.
Can I use shadcn/ui with Vite instead of Next.js?
Yes — run pnpm dlx shadcn@latest init -t vite to scaffold a Vite project, or run pnpm dlx shadcn@latest init inside an existing Vite app. Component usage is identical; only the project setup differs.
How do I update a shadcn/ui component to a newer version?
Re-run the add command for that component — pnpm dlx shadcn@latest add button overwrites it with the current registry copy. Back up any custom edits first, because the fresh copy replaces your modified file.
Sources
Facts in this guide were verified on October 30, 2026 against the following sources. No statistics, versions, or prices are claimed from memory:
- shadcn/ui official documentation — "Installation" —
pnpm dlx shadcn@latest init -t [framework]scaffolding; supported templatesnext,vite,start,react-router,astro; shadcn/create visual preset builder generatinginit --preset [CODE] --template next; Laravel requireslaravel newfirst — https://ui.shadcn.com/docs/installation - shadcn/ui official documentation — "Next.js" install guide —
pnpm create next-app@latestrecommended defaults (Tailwind CSS, App Router,@/*alias);tsconfig.json@/*path mapping;pnpm dlx shadcn@latest initprompts (base, preset, monorepo);pnpm dlx shadcn@latest add card/add button; imports from@/components/ui/*; monorepoadd card -c apps/weband@workspace/ui/components/cardimports — https://ui.shadcn.com/docs/installation/next - shadcn/ui official documentation — "Theming" — CSS variables theming via
tailwind.cssVariables: trueincomponents.json;@import "tailwindcss"+@import "shadcn/tailwind.css";@custom-variant dark (&:is(.dark *));@theme inlinetoken mapping; semantic token table (background, card, popover, primary, secondary, muted, accent, destructive, border, input, ring, chart-1…5, sidebar family); base colors Neutral, Stone, Zinc, Mauve, Olive, Mist, Taupe;--radius: 0.625remwith derived radius scale; custom token recipe;init --no-css-variablesas an installation-time choice — https://ui.shadcn.com/docs/theming - shadcn/ui official documentation — "Your project is ready!" —
pnpm dlx shadcn@latest add button card input label;add --all; blocks (pnpm dlx shadcn@latest add login-03→@/components/login-03); community registries vianpx shadcn@latest add @[registry]/[name]; visual presets applied withinit --preset [CODE]; MCP server viapnpm dlx shadcn@latest mcp init— https://ui.shadcn.com/docs/new - ui-skills.com — "shadcn Skill" — package runners
npx shadcn@latest/pnpm dlx shadcn@latest/bunx --bun shadcn@latest; use built-in variants before custom styles (variant="outline",size="sm"); semantic colors (bg-primary,text-muted-foreground, never raw values);cn()for conditional classes — https://www.ui-skills.com/skills/shadcn-ui/shadcn - dev.to — whoffagents, "shadcn/ui Setup Guide for Next.js 14: Components, Patterns, and Dark Mode" — copy-not-install model;
npx shadcn@latest initwizard (color scheme, CSS variables, component directory);cn()implementation (clsx+twMergeinsrc/lib/utils.ts); cva Button variants; Form wrapping React Hook Form — https://dev.to/whoffagents/shadcnui-setup-guide-for-nextjs-14-components-patterns-and-dark-mode-1m8k