shadcn/ui isn't a component library. It's a code generator you control. Own your components. Radix under the hood, Tailwind for styling, TypeScript for type safety — accessibility and theming baked in. This is how production design systems ship in 2026.
Most teams use Material UI, Chakra, or Mantine and then spend three years fighting their design system instead of shipping features. Buttons look wrong, the grid doesn't align, and when you need a custom variant, you're digging into theme config that someone else wrote. The library owns your component, and when you need to deviate, you're overriding with `sx` or `styled` or inline CSS. By the time you're three projects deep, you've built a fragmented mess of design hacks.
shadcn/ui flips this. It's not a library you install and import from. It's a CLI that drops component source code directly into your project. You run `npx shadcn-ui@latest add button`, and it copies the Button component into `src/components/ui/button.tsx`. You own the code. Want to add a loading spinner to the button? Edit the file. Need a custom color variant? Modify the props. The component isn't a black box — it's your code, and you can read every line. This is why Aidxn builds every production project on top of shadcn, and why Velocity X's dashboard (Dialog, DropdownMenu, NavigationMenu, ContextMenu, Tabs) ships shadcn across the board.
Why shadcn Beats Material UI, Mantine, and Chakra
Material UI is heavy — 90KB minified. You bundle the entire library, the theme system, and hundreds of components you'll never use. Customization is indirect: reach for the `sx` prop or theme overrides, and you're editing config files instead of reading code. Chakra is lighter (30KB), and the theming is cleaner, but you're still importing from a library and trusting that their API is stable. Mantine ships beautiful components and good docs, but you're locked into Mantine's design language. Moving to a new brand means overriding colors everywhere or forking the library.
shadcn is different because there's no library to lock you into. The CLI copies Radix UI primitives (unstyled, accessible components) and pairs them with Tailwind CSS classes. The result: you get the accessibility for free (Radix handles ARIA attributes, keyboard navigation, focus management), and you style with Tailwind (no CSS-in-JS overhead, no runtime parsing). When you need to customize, you're editing your own code. When you onboard a new brand (HailHero, Staff Operations Dashboard, Rebuild Relief), you copy the components, update the color tokens, and ship. No theme config gymnastics. No library fighting back.
Bundle size: Material UI 90KB, Chakra 30KB, Mantine 40KB. shadcn components (alone) are 15–30KB depending on which ones you add. Radix + Tailwind are already in your deps, so the marginal cost is just the component code.
Customization: Material UI needs `sx` or theme config. Chakra needs `useStyleConfig`. Mantine needs style props. shadcn: edit the file.
Accessibility: Material, Chakra, Mantine all bundle Radix internally. shadcn uses Radix directly, so you're trusting the same source.
Installation and Setup
Start with a React + Tailwind project (or Astro with React islands). Install shadcn-ui CLI:
npx shadcn-ui@latest init
Answer the prompts: TypeScript? Yes. CSS variable setup? Yes (shadcn creates a `globals.css` with design tokens). Then add components as you need them:
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add tabs
Each command adds a new file to `src/components/ui/`. They're TypeScript by default, fully typed, and ready to customize. The CLI also sets up color tokens in `globals.css` using CSS variables — this is where theming lives. For Velocity X, every project re-exports these components from a central index:
// src/components/ui/index.ts
export { Button } from './button';
export { Dialog, DialogContent, DialogHeader } from './dialog';
export { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from './dropdown-menu';
export { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
Everywhere else: `import { Button, Dialog } from '@/components/ui'`. One place to see the catalog.
Four Essential Components, Explained
1. Button — The Fundamental
The Button component is tiny but dense. Radix provides no visual output — it's just a `
<Button variant="primary" size="lg">Get Started</Button>
<Button variant="secondary" disabled>Processing...</Button>
<Button variant="destructive" size="sm">Delete</Button>
Want a new variant? Open `src/components/ui/button.tsx`, add a className to the `variants` object, and use it. No theme config, no library version checks.
2. Dialog — Modal, Popover, Sheet
Radix Primitive drives the entire interaction (open/close state, focus trap, backdrop, keyboard ESC handling). shadcn wraps it with Tailwind classes:
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>Open Settings</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Preferences</DialogTitle>
</DialogHeader>
<p>Your settings here.</p>
</DialogContent>
</Dialog>
The Dialog is accessible by default: backdrop click closes it, ESC closes it, focus is trapped inside the dialog until it's dismissed. No manual ARIA attributes. No re-render headaches.
3. Dropdown Menu — Accessible Popovers
DropdownMenu wraps Radix's PopperPrimitive with Tailwind styling and sub-menu support:
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Keyboard navigation (arrow keys to move, Enter to select, ESC to close) is handled by Radix. Styling (colors, padding, animations) is Tailwind. You write the structure, accessibility and aesthetics happen automatically.
4. Tabs — Segmented Control, Tab Navigation
Tabs composes Radix's Tabs primitive with shadcn styles. Use it for navigation or segmented controls:
<Tabs defaultValue="dashboard">
<TabsList>
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="dashboard">Dashboard content</TabsContent>
<TabsContent value="analytics">Analytics content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
</Tabs>
Radix handles ARIA roles and keyboard navigation (left/right arrow to switch tabs). shadcn styles them with indicators, hover states, and active states.
Token Theming — The Power Move
shadcn stores all colors in CSS variables defined in `globals.css`. For Velocity projects, theming happens in one place: `brand.json`. Update the primary color token, and every component using that token updates automatically. No component-level prop drilling, no theme provider wrapping:
// globals.css — shadcn colors
@layer base {
:root {
--primary: 265 90% 50%; /* Aidxn purple */
--secondary: 0 0% 8.8%; /* Near-black */
--accent: 48 96% 53%; /* Yellow */
--foreground: 0 0% 100%; /* White */
--background: 0 0% 8.8%; /* Dark slate */
}
.dark {
--primary: 265 90% 50%;
--secondary: 0 0% 90%;
--background: 0 0% 8.8%;
}
}
// Then in components:
<Button style={{ backgroundColor: `hsl(var(--primary))` }}>
When onboarding HailHero or Staff Operations Dashboard, the tokens stay the same structure. Only the values change. Every component respects those tokens. Add a new color variable, update the token, and the design system shifts.
Six FAQs
Do I need Radix? Can't I just use shadcn?
shadcn IS Radix UI + Tailwind. Radix provides the unstyled, accessible primitives (Dialog, Menu, Tabs). shadcn drops them into your project with Tailwind styling. You're not choosing between them — you're using both.
What happens when shadcn updates?
The CLI installs components into your repo. You own the code. Updates are optional. Run the CLI on a component to pull the latest version, review the diff, and commit. You control the pace and what changes land.
Can I use shadcn with Next.js, Astro, Remix?
Yes. shadcn needs React 18+ and Tailwind 3+. Works with Next.js, Astro with React islands, Remix, SvelteKit, Vue. Anywhere you have React components and Tailwind.
How do I customize a component beyond variants?
Edit the source file in `src/components/ui/`. Add props, change Tailwind classes, add custom animations, swap out Radix sub-components. It's your code now.
Is shadcn good for design systems at scale?
Yes. Velocity X uses shadcn as the foundation for multi-brand dashboards. Copy the `ui/` folder, swap the CSS variables, and each brand has a themed system. Scaling means managing the design tokens and keeping the component source in sync across forks.
Should I use Zustand with shadcn components?
Absolutely. shadcn components handle their own UI state (open/close, selected tab). Zustand holds your app state. See Zustand in 2026 for the pattern.
The Bottom Line
shadcn/ui isn't a library — it's a philosophy: own your components, inherit accessibility from Radix, style with Tailwind, and build a design system that scales across brands. Material UI, Chakra, and Mantine all try to solve design through abstraction. shadcn solves it through transparency. You get the accessibility wins (free from Radix), the styling speed (Tailwind), and full control (your code). For dashboard-heavy work, multi-brand product suites, and anything that needs long-term maintenance, this is the stack. Pair it with production dashboards built on shadcn dialogs and menus, and you've got a system that scales from V0 to V100.