
What is shadcn/ui and how to install and use it (with examples)
shadcn/ui isn't a component library like the others: you don't install it, you copy it. How the CLI works, what Radix UI and Tailwind do under the hood, and when it's actually worth using.
The first thing to understand about shadcn/ui is that it isn’t a library. There’s no shadcn-ui package to drop into package.json and bump with npm update. What you install is a CLI that copies the component’s source code — a readable, non-minified .tsx file, not something imported from node_modules — straight into your project. From that point on, the component is yours: you open it, read it, and edit it like any other file in the repo.
How the CLI works
Initial setup:
npx shadcn@latest init
This asks a few preference questions (style new-york or default, base color, whether to use CSS variables) and generates a components.json file that records those choices, plus sets up Tailwind and creates lib/utils.ts with a cn() helper — combining clsx and tailwind-merge to handle conditional class names without conflicts.
To add a component:
npx shadcn@latest add button
The CLI downloads the Button component’s code (and its dependencies, e.g. @radix-ui/react-slot) and writes it to src/components/ui/button.tsx. No version to track, no changelog to read before upgrading — because you’re not upgrading anything, you’re reading TSX code you just copied.
What’s underneath: Radix UI + Tailwind + CVA
Every shadcn/ui component is built on three pieces:
- Radix UI Primitives: headless (unstyled) components that handle accessibility and interaction — focus trapping, keyboard handling, correct ARIA attributes on
Dialog,Popover,DropdownMenu, and similar. It’s the hardest part to get right by hand, and shadcn/ui doesn’t reinvent it — it just reuses it. - Tailwind CSS: for visual styling. Each component is a handful of Tailwind classes applied to the Radix elements.
class-variance-authority(CVA): handles variants (variant="destructive",size="sm") in a typed way, generating the right class combinations instead of scattered if/else logic inside the component.
The result is a Button you technically wrote yourself, but that inherits Radix’s battle-tested accessibility and a Tailwind design system’s visual consistency.
Using it in practice
Once copied, Button is just a regular React component: import it and use its typed props.
import { Button } from "@/components/ui/button";
export function Toolbar() {
return (
<div className="flex gap-2">
<Button variant="default">Save</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline" size="sm">
Cancel
</Button>
</div>
);
}
More complex components follow the same pattern. Dialog, for example, composes several already-styled Radix parts (Trigger, Content, Header, Footer):
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export function DeleteAccountDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
This action cannot be undone.
</p>
</DialogContent>
</Dialog>
);
}
There’s no “magic” API to learn beyond this: focus trapping, closing on Esc, correct aria-* attributes are already handled by Radix inside dialog.tsx, which you can open and read at any time.
The real advantage: you own the code
With a traditional component library (Material UI, Ant Design), customizing a style past a certain point means fighting the library’s API — override props, theme objects, !important. With shadcn/ui, the component is a file in your repo: if Dialog doesn’t do exactly what you need, you just edit it — no subclassing, no waiting for an upstream release.
That also makes it particularly convenient with AI-assisted code generation tools (v0, Cursor, Claude): the model sees normal TSX code in the project, not a black-box component imported from a package, so it can propose direct edits with much more context.
The trade-off
Owning the code also means maintaining it. If Radix UI ships an accessibility fix for Dialog, it doesn’t arrive via npm update — you have to re-run add dialog (which overwrites the file, so you lose any local changes unless you isolated them) or patch it by hand. For projects with dozens of customized components, that’s a real maintenance cost a traditional library doesn’t have.
It’s also worth noting that, despite the historical name “shadcn/ui”, the project was built specifically for React + Tailwind. Community-maintained ports exist for other frameworks (Vue, Svelte), but they don’t have the same coverage as the original — if your project isn’t React, check the port’s maturity before adopting it.
Pros and cons
Pros:
- No dependency on a UI npm package: no version to keep in sync, no upstream breaking change hitting you by surprise.
- Unlimited customization: you edit the file directly, there’s no override API to guess your way around.
- Accessibility baseline already covered: inherits Radix UI’s work on focus, keyboard handling, and ARIA — redoing that from scratch is a huge amount of work.
- Great fit for AI-assisted tooling: readable code in the repo, not a black-box package, so it’s easier for v0/Cursor/Claude to edit.
- No library runtime overhead: no global provider, no library bundle to load — just the components you actually added.
Cons:
- No automatic updates: an upstream fix from Radix or the component itself doesn’t arrive via
npm update— you have to reapply it by hand or re-runadd(risking overwriting your changes). - More code to maintain in the repo: every component you add is your code now, with the testing and maintenance burden that comes with it.
- Tied to Tailwind: if your project doesn’t already use it, adopting shadcn/ui means adopting Tailwind too.
- Uneven coverage outside React: Vue/Svelte ports exist but don’t match the original’s depth.
- No semantic versioning: you can’t say “we’re on shadcn/ui 2.3” — every component has its own history of local edits.
FAQ
❓ Do I need to install shadcn/ui as a dependency?
No. The shadcn package (the CLI) is a one-off tool run via npx/pnpm dlx to generate or add
components; the final code copied into the repo has no runtime dependency on any shadcn-ui
package.
❓ Does shadcn/ui work without Tailwind?
Recent CLI versions support “plain” CSS variables without Tailwind for some setups, but the documented, intended experience is still Tailwind-based — outside that context, expect to manually adapt some of the classes.
❓ How do I update a component after it's changed upstream?
There’s no “sync” command: the safest option is manually diffing the updated file on the
official registry against your local version and applying only the changes you need, or
re-running add on a component you haven’t customized yet.
When it’s worth using
If your project is already on Tailwind and you want to start from accessible components without writing a WAI-ARIA-compliant Dialog or Combobox from scratch, shadcn/ui is probably the fastest option available today. If your team prefers a “closed-box” library with semantic versioning and centralized updates — or doesn’t use Tailwind and has no intention of adopting it — a traditional component library remains the simpler choice.

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.