Adding a component
The path from a Base UI primitive to a documented, registry-published component.
Create the folder
src/components/ui/<name>/
├── <name>.tsx the component
├── <name>.stories.tsx Storybook — the test surface
├── <name>.story.tsx docs preview (generated)
├── <name>.demo.tsx docs preview (generated)
└── index.ts export * from "./<name>"Write the component
Wrap the Base UI primitive. Two hard rules, both enforced by CI:
- No
next/*imports. Component source must run in Vite and plain React too.scripts/check-registry.mjsfails the build otherwise. - No literal colours. Use semantic utilities —
bg-surface-raised,text-fg-muted,border-line. If no token fits the role, that is a contract conversation, not a hex code.
Add "use client" when the component uses Base UI's interactive primitives or
React state. Leave it off when the component is genuinely presentational —
Button has no directive so consumers can use it inside an RSC.
For any anchored popup, apply the shared popup-motion utility to the Popup
part rather than writing bespoke transitions.
See Motion.
Write the stories
<name>.stories.tsx is the test surface: a Default story, one per meaningful
variant, and play functions asserting behaviour.
This file is also the source of the docs preview, so make the first story representative — the generator lifts it.
Register it
Add an entry to registry.json:
{
"name": "<name>",
"type": "registry:ui",
"title": "<Title>",
"description": "<Name> component built on Base UI, styled with sc1m tokens.",
"dependencies": ["@base-ui/react"],
"registryDependencies": [
"https://sc1m.vercel.app/r/tokens.json",
"https://sc1m.vercel.app/r/cn.json"
],
"files": [{ "path": "src/components/ui/<name>/<name>.tsx", "type": "registry:ui" }]
}title and description become the docs page's heading and subtitle, so write
them for a reader.
Generate the docs page
npm run docs:genThis reads registry.json and your stories file and writes the MDX page plus
the preview modules. It never overwrites a file that already exists — hand
edits always survive. Use --force to regenerate deliberately.
Verify
npm run registry:build
node scripts/check-registry.mjs
node scripts/check-tokens-parity.mjs
npx vitest run --project=storybook
npm run buildHow the preview generation works
The generator lifts the first story out of <name>.stories.tsx and shapes it
one of two ways:
render: (args) => <Component … {...args} />— a single element, so it becomes a controls story.@fumadocs/storyderives the control set from the component's TypeScript props, and the reader gets live inputs.render: () => (…)— a composition, so it becomes a fixed preview wrapped in aDemocomponent. Controls are not meaningful for a fixed composition.
To hand-author a preview, just edit the generated .demo.tsx; the generator
will leave it alone from then on.