# shadcn/ui

## 1. Not a Package — a Code Generator

Every library covered so far in this group installs as an npm dependency: `import Button from "@mui/material/Button"` pulls compiled component code out of `node_modules`, and you never see or edit its source. shadcn/ui inverts this completely: it is a **CLI tool** that **copies component source code directly into your own project**. There is no `shadcn` package sitting in `node_modules` providing components at runtime — after running the CLI, the component's actual `.tsx` file lives in your repo, under your version control, editable like any file you wrote yourself.

```bash
npx shadcn@latest add button
# creates: components/ui/button.tsx  — real source, now YOURS
```

```tsx
// components/ui/button.tsx (excerpt — this file is generated once, then you own it)
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
);
```

## 2. Built From Radix UI Primitives + Tailwind

shadcn components aren't written from scratch — each one wraps an unstyled, fully-accessible **Radix UI primitive** (handling focus trapping, keyboard navigation, ARIA attributes, portal rendering for overlays) and applies **Tailwind utility classes** for visual styling, composed via `class-variance-authority` (`cva`) for variant-based styling (the `variant`/`size` props above) and the `cn()` helper (a `clsx` + `tailwind-merge` combo, see the Tailwind CSS topic's `twMerge` exercise for the underlying idea) for merging classes without conflicts.

```
Radix UI primitive   → unstyled behavior/accessibility (focus, keyboard nav, ARIA)
Tailwind + cva        → the visual styling layered on top
shadcn CLI             → glues the two together and writes the result into YOUR repo
```

## 3. The Ownership Model — What "You Own It" Actually Means

```tsx
// You can open components/ui/button.tsx and just... edit it.
// Want a new variant? Add a key to buttonVariants' "variant" object.
// Want different default padding? Change the "default" size string directly.
// No wrapper component, no "swizzling," no fighting an npm package's API surface.
```

Because the component is plain source code in your repo, customizing it beyond what a `variant`/`size` prop exposes means **directly editing the file** — there's no separate "theming API" to learn, no risk of a `npm update` silently changing the component's behavior out from under you, and no need to reach for MUI-style theme overrides or `!important` CSS overrides to fight a black-box package's default styles. The cost is the inverse of MUI's: you're now responsible for that file's maintenance (bug fixes, accessibility regressions) instead of a maintainer upstream — there's no `npm update @mui/material` equivalent that pulls in improvements automatically once a component has been generated into your project.

## 4. Composing shadcn Components

```tsx
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";

function DeleteConfirmDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete account</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
        </DialogHeader>
        <p>This action cannot be undone.</p>
      </DialogContent>
    </Dialog>
  );
}
```

Components compose as ordinary JSX, same as any React component — `Dialog`/`DialogTrigger`/`DialogContent` are compound-component pieces (inherited from the underlying Radix primitive's API shape) that share implicit state (open/closed) via context, rather than one monolithic `<Dialog open={...} content={...}>` prop-driven component.

## 5. Theming via CSS Variables

```css
/* globals.css, generated by the shadcn CLI */
:root {
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --radius: 0.5rem;
}
.dark {
  --primary: 210 40% 98%;
  --primary-foreground: 222.2 47.4% 11.2%;
}
```

shadcn's generated Tailwind config maps utility classes like `bg-primary` to these CSS custom properties rather than fixed hex values, so a single `:root`/`.dark` variable swap re-themes every generated component at once — conceptually similar to MUI's theme object, but implemented as plain CSS variables rather than a JS object resolved through React Context.

## Under the Hood

**The shadcn CLI is fundamentally a file-copy-and-template-fill operation, not a package manager action.** Running `npx shadcn add button` fetches a registry entry (a JSON manifest describing the component's source file(s), its dependencies like `class-variance-authority` or `@radix-ui/react-slot`, and where each file should be written in your project), installs any missing npm dependencies it declares, and writes the component's actual `.tsx` source into `components/ui/` using your project's configured path aliases (from `components.json`, generated on first `shadcn init`). There is no runtime registry lookup, no version pin to a "shadcn version" tracked anywhere after that point — the file is now indistinguishable from any other file you wrote, and future `shadcn add` runs for OTHER components don't touch it.

**This is precisely why there's no automatic-update story.** A traditional npm package's `package.json` entry records an installed version, and `npm update` can fetch a newer one; shadcn's registry entries carry no such persistent link back to your already-generated files. If the upstream `button.tsx` template improves (an accessibility fix, a new Radix API), your project's copy doesn't know or care — picking up the improvement means manually diffing the new template against your (possibly already-edited) local file and merging by hand. This tradeoff is the direct, unavoidable consequence of the ownership model in section 3: full editability requires giving up automatic propagation of upstream fixes.

![An npm component library stays in node_modules and updates via npm, while shadcn copies template source into your repository where you own and edit it with no automatic updates](diagrams/01-ownership-model.svg)

## Common Pitfalls

1. **Expecting `npm update` or a similar command to update shadcn components** — there is no such mechanism; updates to already-generated components require manually re-running `shadcn add <component>` (which will prompt to overwrite) and re-applying any local edits, or diffing by hand.
2. **Treating the generated files as read-only vendor code** — the entire point of the model is that they're yours to edit; leaving them untouched and reaching for wrapper components or CSS overrides instead defeats the ownership benefit and reintroduces the "fighting a black box" problem shadcn exists to avoid.
3. **Not running `shadcn init` first** — the CLI needs `components.json` (path aliases, Tailwind config location, CSS variable vs. utility-class color mode) before `add` commands know where to write files; skipping init produces confusing path errors.
4. **Forgetting a component's Radix dependency comes along with it** — deleting a generated component file without also removing its now-unused `@radix-ui/react-*` package leaves a dangling dependency; conversely, editing away all Radix usage from a file without removing the import breaks it.
5. **Assuming visual identity is Tailwind-only** — a decent portion of a shadcn component's appearance comes from the CSS variable theme (section 5), not just inline utility classes; changing `--primary` in `globals.css` has effects a pure Tailwind utility edit on one component won't replicate app-wide.

## Quick Reference

```
npx shadcn init              → sets up components.json, Tailwind config, CSS variables
npx shadcn add <component>    → copies real source into components/ui/, installs deps

Ownership: generated files are YOURS — edit them directly, no theming API to learn
Built from: Radix UI primitive (behavior/a11y) + Tailwind + cva (variants) + cn() (class merge)
No auto-update: unlike an npm package, there's no version-tracked update path after generation
Theming: CSS custom properties (--primary, --radius, ...) in globals.css, swapped per .dark etc.

Contrast: MUI  = black-box package, theme object, npm-updatable
          shadcn = generated source, CSS variables, manually re-synced
```
