Why shadcn/ui Needs 'You Own This Code' Rules
shadcn/ui is fundamentally different from component libraries like MUI, Chakra, or Ant Design. It's not installed as a dependency — components are copied into your project with npx shadcn-ui@latest add button. The source code lives in your components/ui/ directory. You own it, you modify it, you're responsible for it. AI assistants don't understand this model — they treat shadcn/ui like a library: importing from a package, never modifying components, and creating wrapper components instead of editing the source.
The most common AI failures: trying to import from "@shadcn/ui" (doesn't exist — it's your local code), creating wrapper components around shadcn components instead of modifying the source directly, not using the cn() utility for class merging, ignoring the cva (class-variance-authority) pattern for variants, and overriding styles with inline styles instead of editing the component's Tailwind classes.
These rules ensure AI treats shadcn/ui components as owned source code — modifiable, customizable, and integrated into your design system.
Rule 1: Components Are Your Code — Modify Directly
The rule: 'shadcn/ui components live in components/ui/ — they are your source code, not a dependency. Modify them directly when you need changes: add props, change styles, adjust behavior. Never create a wrapper component just to change a style — edit the component source. Never import from a shadcn package — import from @/components/ui/button (your local path). The source is the single version — there's no 'upstream' to stay in sync with.'
For adding components: 'Use the CLI to add new components: npx shadcn-ui@latest add dialog. This copies the component source into components/ui/. After adding, the component is yours — customize it. The CLI is for initial scaffolding only, not for updates. If you need a newer version of a component, compare the diff and merge changes manually.'
AI generates import { Button } from "shadcn-ui" or creates MyButton wrapping Button. In shadcn/ui, Button IS your component — edit components/ui/button.tsx directly. No wrapper needed, no package to import from.
- components/ui/ is YOUR code — modify directly, don't wrap
- Import from @/components/ui/ — never from a package
- npx shadcn-ui add for scaffolding — then it's yours to customize
- No upstream sync — you own the source, you maintain it
- Edit the component, don't create a wrapper around it
Need to change Button's padding? Edit components/ui/button.tsx directly. AI creates <MyButton> wrapping <Button> — unnecessary indirection. The source is yours. There's no upstream to break compatibility with.
Rule 2: cn() for All Class Merging
The rule: 'Use the cn() utility (from lib/utils.ts) for all Tailwind class composition: cn("base-classes", conditional && "conditional-classes", className). cn() merges classes intelligently — later classes override earlier ones (p-4 + p-2 = p-2, not both). Never use template literals for class composition: `${baseClass} ${conditional ? "active" : ""}` doesn't handle Tailwind conflicts. cn() is a thin wrapper around clsx + tailwind-merge.'
For component props: 'Every shadcn/ui component accepts a className prop. Use cn() to merge the component's default classes with the consumer's className: <div className={cn("rounded-lg border p-4", className)}>. This lets consumers override any default style: <Card className="border-red-500"> overrides the default border color without !important.'
AI uses string concatenation or template literals for classes — both break when Tailwind classes conflict (two different padding values both apply). cn() with tailwind-merge resolves conflicts: the last value wins, cleanly.
Template literals don't: `p-4 ${props.className}` where className='p-2' applies both paddings. cn('p-4', props.className) with tailwind-merge resolves to just p-2. Always use cn() for class composition.
Rule 3: cva for Component Variants
The rule: 'Use class-variance-authority (cva) for components with multiple visual variants. Define variants as a cva config: const buttonVariants = cva("inline-flex items-center rounded-md font-medium", { variants: { variant: { default: "bg-primary text-primary-foreground", destructive: "bg-destructive text-destructive-foreground", outline: "border border-input bg-background" }, size: { default: "h-10 px-4 py-2", sm: "h-9 px-3", lg: "h-11 px-8" } }, defaultVariants: { variant: "default", size: "default" } }).'
For using variants: 'Apply with cn(): className={cn(buttonVariants({ variant, size }), className)}. Export the variants type for consumers: export type ButtonProps = VariantProps<typeof buttonVariants>. This gives type-safe variant selection: <Button variant="destructive" size="lg">. Never use conditional class strings for variants — cva handles the matrix of combinations cleanly.'
AI generates if/else chains for component variants: if (variant === "primary") classes += "bg-blue-500". cva replaces all of this with a declarative config that's type-safe, composable, and readable.
Rule 4: Radix UI Primitives Under the Hood
The rule: 'shadcn/ui components are built on Radix UI primitives for accessibility and behavior: Dialog, Popover, Select, Dropdown, Tooltip, Tabs, Accordion, etc. The Radix primitive handles keyboard navigation, focus management, ARIA attributes, and animation states. Your customization is the styling layer (Tailwind classes). Never replace Radix primitives with custom implementations — you lose the accessibility guarantees.'
For extending components: 'When you need custom behavior, extend the Radix primitive — don't replace it. Add new props to the shadcn component, use Radix's composition pattern (asChild for polymorphic rendering), and style with Tailwind. Radix provides the behavior contract; Tailwind provides the visual layer.'
For animation: 'Use Tailwind's animation utilities with Radix's data attributes: data-[state=open]:animate-in, data-[state=closed]:animate-out. Radix sets these attributes automatically during transitions. Use tailwindcss-animate plugin for enter/exit animations that work with Radix state attributes.'
- Radix primitives for behavior: Dialog, Select, Popover, Tabs, etc.
- Never replace Radix with custom — keyboard nav, focus, ARIA are built in
- Extend with new props and Tailwind — don't rewrite the primitive
- asChild for polymorphic rendering — <Button asChild><Link>...</Link></Button>
- data-[state=open/closed] for animation triggers — tailwindcss-animate plugin
Radix primitives handle keyboard navigation, focus trapping, ARIA attributes, and animation states. Replacing Dialog with a custom div loses all of this. Style with Tailwind, extend with props — never rewrite the primitive.
Rule 5: CSS Variable Theming
The rule: 'shadcn/ui uses CSS custom properties for theming — defined in globals.css: :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; }. Use HSL values without the hsl() wrapper (shadcn's convention). Reference in Tailwind config: colors: { background: "hsl(var(--background))", foreground: "hsl(var(--foreground))" }. Use semantic names: bg-background, text-foreground, bg-primary — never raw Tailwind colors.'
For dark mode: 'Define dark theme variables in .dark: .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; }. Toggle with class="dark" on the root element. Use next-themes for React-based theme switching. All components automatically adapt — no component-level dark mode logic needed.'
For custom themes: 'Add new CSS variables for your brand: --brand: 250 50% 50%. Reference in tailwind.config: brand: "hsl(var(--brand))". Use in components: bg-brand, text-brand. This extends shadcn's theming system with your project-specific colors while maintaining the HSL variable pattern.'
Complete shadcn/ui Rules Template
Consolidated rules for shadcn/ui projects.
- components/ui/ is YOUR code — modify directly, import from @/components/ui/, never wrap
- cn() for all class merging — never template literals or string concatenation
- cva for component variants — type-safe, declarative, composable
- Radix primitives for behavior — never replace with custom implementations
- CSS variables for theming: --background, --primary — HSL without hsl() wrapper
- Semantic color names: bg-background, text-foreground — never raw Tailwind colors
- next-themes for dark mode toggle — .dark class on root — auto-adapting components
- npx shadcn-ui add for scaffolding — then customize the source freely