shadcn-composition-patterns
An agent skill that makes your coding agent build components the way shadcn builds its own.
Setup
Install the skill:
npx skills add AlexDemzz/skills --skill shadcn-composition-patterns -gThere’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.
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: |
| Each part is one element |
|
| Variants at the call site | A one-off look is the consumer’s |
| Polymorphic root, navigation-agnostic | The consumer picks the element: |
Styling from outside targets data-slot |
|
Common questions
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.
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.
Hand it the element. The component never guesses a link from a url prop:
// 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>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.
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 (opens in a new tab), next to design-to-shadcn, which skins shadcn primitives from a reference design, and grokbot-notify, which messages you in your Grok Bot chat when your agent finishes, gets blocked or needs your decision.