Overview
shadcn/ui is not a component library in the npm sense. It is a CLI that copies accessible components built on Radix and Tailwind into your repo. You own the source, you edit the source, and there is no package to upgrade. This page covers the conventions that follow from that model.
Own the source, do not depend on the library
The first principle of shadcn is that the components live in your codebase under components/ui/. Treat them like any other file you wrote: read them, edit them, audit them in PRs.
- Do not add
shadcn(orshadcn-ui) as a runtime dependency. The CLI is a devDependency at most. - Commit
components/ui/*.tsxto the repo. They are first-party code. - When the upstream component changes, port the diff manually. There is no
npm update.
This is the trade: you trade upgrade convenience for total control over the markup, the classes, and the behavior.
Install via the CLI per component
Add components one at a time as you need them. The CLI writes a single file and any peer dependencies it needs (Radix primitives, class-variance-authority, tailwind-merge).
npx shadcn@latest init # one time per project
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add data-tableDo not run add for every component in the catalog. Add only what the next feature uses. Unused components are dead weight in a layer you own.
Theme with CSS variables in globals.css
shadcn ships a token system as CSS variables on :root and .dark. Edit those values; do not patch components to hardcode colors.
@layer base {
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.2 0 0);
--primary: oklch(0.55 0.18 264);
--primary-foreground: oklch(0.98 0 0);
--radius: 0.5rem;
}
.dark {
--background: oklch(0.15 0 0);
--foreground: oklch(0.95 0 0);
}
}With Tailwind v4, expose the variables through @theme so utilities like bg-primary and text-foreground map to them. See tailwind for the v4 CSS-first config.
Compose with compound components
Most shadcn components are exported as a namespace with sub-components. Compose, do not pass props soup.
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>The asChild prop slot-projects styles onto a child element so a <Button> can be the trigger without nesting <button> inside <button>. Use it whenever a wrapper would break semantics.
Accessibility comes from Radix; do not break it
The interactive primitives (Dialog, Popover, Combobox, Menu, Tabs) wrap Radix UI. Radix handles focus trap, keyboard navigation, ARIA wiring, and dismissal. Your job is to not break it.
- Do not replace a Radix
Dialogwith a div and a portal. You will lose focus trapping andEschandling. - Do not strip
aria-*props the components forward. - Test keyboard navigation: every interactive flow should work with Tab, Shift+Tab, Enter, Space, Esc, and arrow keys.
If you cannot achieve a design with the primitive, fork the file rather than reinventing the behavior elsewhere.
Fork the component when the design pulls hard
Editing in place is cheap; that is the point. Fork when:
- The default variants (size, intent) do not cover your design system.
- The markup needs a wrapping element for a layout reason.
- You need a new sub-component that fits naturally next to the existing ones.
Use as-is when the only change is colors or spacing; that is the theme’s job, not the component’s. Do not fork to override one class string; edit the file directly.