# shadcn-composition-patterns

An agent skill that makes your coding agent build components the way shadcn builds its own.

## Setup

Install the skill:

```bash
npx skills add AlexDemzz/skills --skill shadcn-composition-patterns -g
```

There’s nothing to configure. On each run, the agent reads your project’s base from the `package.json` beside `components.json`, in your ui package if you have a monorepo. Any `@base-ui` dependency means Base UI, otherwise `radix-ui` or `@radix-ui/*` means Radix. The base decides how a part becomes a link.

Then it lists your components directory before writing anything. If a primitive or a compound already covers the need, it stops and points you to it.

## What it does

Your agent builds every component as a compound: one file, one prefixed family of parts, each part a single element you style and replace from the call site.

```tsx
import { cn } from "@/lib/utils";

function Navbar({ className, ...props }: React.ComponentProps<"nav">) {
  return (
    <nav
      className={cn("flex items-center gap-4 px-4 py-3", className)}
      data-slot="navbar"
      {...props}
    />
  );
}

function NavbarTitle({ className, ...props }: React.ComponentProps<"h1">) {
  return (
    <h1
      className={cn("text-base font-semibold", className)}
      data-slot="navbar-title"
      {...props}
    />
  );
}

export { Navbar, NavbarTitle };
```

Hand it an existing file instead and it grades that file against the same laws, part by part.

## When to reach for it

Every time a React component is created, changed or audited in a project with a `components.json`. Give it a component to build, or a file to review.

| Law | What it asks |
| --- | --- |
| Compound, prefixed named exports | One root plus parts, each a top-level export: `NavbarTitle`, never `Navbar.Title`. |
| Each part is one element | `data-slot`, `className` through `cn` and props spread last. Spacing between parts lives on the parent. |
| Variants at the call site | A one-off look is the consumer’s `className`. A `cva` variant needs several consumers. |
| Polymorphic root, navigation-agnostic | The consumer picks the element: `render` on Base UI, `asChild` on Radix. No auto-`<Link>`. |
| Styling from outside targets `data-slot` | `[&_[data-slot=item-title]]:font-semibold` on the root’s `className`. |

## Common questions

**Where does the new component go?**

Used by one consumer, it lives in that consumer’s file. Shared, it follows your existing component convention, or sits beside the shadcn primitives at your `aliases` path.

**Can I add a variant prop?**

Only for a purely visual look that several consumers share. A variant with one consumer is that consumer’s `className`. A prop that only makes sense for one variant gets a part of its own.

**How do I make a part a link?**

Hand it the element. The component never guesses a link from a `url` prop:

```tsx
// Base UI
<JobCard render={<Link to="/jobs/$id" params={{ id }} />}>
  <JobCardTitle>{job.title}</JobCardTitle>
</JobCard>

// Radix
<JobCard asChild>
  <Link to={`/jobs/${id}`}>
    <JobCardTitle>{job.title}</JobCardTitle>
  </Link>
</JobCard>
```

**Why the callback form for a plain anchor?**

On Base UI, `render={<a href={url} />}` trips the anchor-content lint, and silencing the lint fails the law. `render={(props) => <a {...props} href={url} />}` spreads the children in, so the lint passes.

**Why does my data-state class do nothing?**

Tailwind v4 needs the bracket form for custom data attributes: `data-[state=open]:…`. A bare `data-state-open:` fails silently.

## It’s working if

After a build, you get one file of prefixed parts, such as `Navbar` and `NavbarTitle`, already used in one place in your app with its own `className`. Lint and typecheck pass on the file.

After a review, you get every law per part, pass or fail, with the offending line and the fix.

## Where it fits

shadcn-composition-patterns is the shadcn layer on top of `vercel-composition-patterns`, which covers the React theory: why compounds, why no boolean props, lifting state into a provider.

It lives in [my skills repository on GitHub](https://github.com/AlexDemzz/skills/tree/master/shadcn-composition-patterns), next to [design-to-shadcn](https://alexandrejoly.me/skills/design-to-shadcn.md), which skins shadcn primitives from a reference design, and [grokbot-notify](https://alexandrejoly.me/skills/grokbot-notify.md), which messages you in your Grok Bot chat when your agent finishes, gets blocked or needs your decision.

[Back to Alexandre Joly](https://alexandrejoly.me/index.md)
