July 8, 202611 min read

Belin: a shadcn theme editor that grew into an AI-ready kit.

If you've used shadcn/ui, you already understand most of Belin. shadcn gives you component source you own, built on Base UI primitives and styled with Tailwind. Belin adds the one thing that stack leaves to you: a live theme editor and a real token layer, so you can edit the whole system in the browser and export it — as a single CSS file, or as a complete Belin Kit: components, theme, and AI instructions in one zip, ready for Claude to set up and build with. This is the story of building it, auditing it, fixing what the audit found, and then turning it into something an AI can use.

Start with what you already know.

Three tools do most of the work, and Belin doesn't replace any of them:

shadcn/uiisn't a dependency you install — it's source code copied into your repo, under components/ui/. You own those files. Base UI is the primitive layer underneath them: it handles the invisible hard parts — keyboard navigation, focus, open and close state, ARIA. Tailwind styles it all. What none of them ship is a design system: named values that describe roles, a place to edit those values live, and a way to carry the result into the next project.

That gap is Belin's whole job. It wraps each shadcn component in a thin layer under belin/components/, adds a two-layer token system, and puts a live editor on top. You keep editing shadcn source the way you always did; Belin just makes the theme a first-class, editable, exportable thing.

The two-layer token model.

Tokens split in two. Primitives are raw values named for what they are — --belin-blue-600, --belin-space-4. Semantics are roles that point at primitives — --color-action-primary references --belin-blue-600. Components read only the semantic. Change the primitive once and every role that references it updates; rename a role and you change it in one place. Dark mode is the same names again, overridden under a .dark selector, so components never check which mode is active.

The one bridge that makes it work.

shadcn components expect CSS variables named --primary, --background, --border. Belin names things by role. The two systems meet in one block at the bottom of semantics.css, where the shadcn names alias the Belin roles:

css
--primary:            var(--color-action-primary);
--primary-foreground: var(--color-action-primary-foreground);
--background:         var(--color-background);
--foreground:         var(--color-foreground);
--border:             var(--color-border);

Those few lines are the trick. Every shadcn component reads the Belin theme for free. Edit --color-action-primary in the customiser and the shadcn Button updates — no fork, no patch, no edits to components/ui/. App code imports only from @/belin/components, never the shadcn source directly, which is what keeps the layers from tangling.

Where it started, and what was actually broken.

By its second version Belin looked finished: every component wrapped, a full DataTable, a per-component customiser, text styles, and a theme export. So it got a proper audit — architecture, code quality, docs, and the distribution model. The audit was blunt, and useful.

The headline finding hit the one feature that justifies the whole project. The export — the bridge to other projects — silently dropped almost everything. It saved colours, type, spacing, density, and the Button, and threw away every text-style tweak and every other component's customisation. Export a themed Card and re-import it, and the Card came back plain. The docs had drifted too: they claimed the primitives were Radix when the code had moved to Base UI. And there was no proven way to actually use Belin in a second project — the "design system" was, in practice, this app's own preview.

It looked like a design system. It behaved like a customiser whose export quietly lost most of your work.

Fixing it, in order.

The repair ran in phases, each one setting up the next:

Reconcile the docs. Correct the Radix-versus-Base-UI claim and the other drift first, so the source of truth is trustworthy before touching code.

Cut the duplication.The same "make a style tag" block and the same token-picker dropdown had been copy-pasted across a dozen editors. One shared helper each removed about a thousand lines — and, as a bonus, every editor picked up the rem-or-pixels toggle that only one had.

One catalog as the source of truth.Every customisable component now lives in a single list. "Restore my settings on load" and "clear everything" both read from that one list instead of three hand-kept ones. That alone fixed a class of quiet bugs: colours for roughly twenty-five components could be customised but never restored on a fresh load or cleared on reset. With one list, they're covered by construction.

Complete the export. With the catalog in place, the export could walk it. Now the theme captures colours, dark mode, type, spacing, density, text styles, and every component — and importing restores it exactly. A test proves the round trip: customise across every dimension, export, wipe everything, re-import, and the result is identical.

Prove consumption. A theme now compiles to a plain CSS file. A second project loads that one file and gets the themed components — no JavaScript, no providers, no shared state. A test applies a theme the live way, throws away all the runtime state, loads only the exported CSS, and confirms the components render identically.

What you get now.

Edit one token in the customiser and every shadcn component updates, light and dark at once, with a live preview. When you like it, export the theme. JSON re-imports into the customiser; CSS drops straight into another Tailwind and shadcn project. In that project you copy the component layer, load the base tokens, and load your theme CSS last:

tsx
// app/layout.tsx in the consuming project
import "@/belin/tokens/primitives.css";   // base tokens first
import "@/belin/tokens/semantics.css";
import "@/theme/my-theme.css";            // exported theme, last

import { Button } from "@/belin/components/button";

No applyTheme() call, no provider, no localStorage — the theme is static CSS. Those runtime pieces exist only here, for live editing. The full recipe lives in CONSUMING.md.

The export became a kit: components, theme, and AI instructions in one zip.

A lossless export answered "can the theme leave?" — but a second audit asked a harder question: can a designer's export survive being handed to an AI in a fresh project?The answer was no, for specific reasons: the export carried theme values but no components, the code quietly needed sixteen npm packages that were listed nowhere, and nothing existed that an AI could read to learn the system's rules.

Six phases fixed that, each one proven before the next. The dependency list got measured from the imports rather than written from memory (the audit guessed seventeen; the code said sixteen). A generator now writes the AI-facing reference — component inventory, tokens, text styles — from the same source files that run the customiser, with a test that fails any commit where docs and code drift. A Claude skill wraps those docs with setup and usage instructions. A build step packs it all, and one button in the download dialog assembles the final zip in the browser: static skeleton plus your live theme plus a stamped manifest.

The last phase was a test with no help given. A script scaffolds a brand-new Next.js project, drops in a real downloaded kit with a deliberately purple theme, and gives a fresh AI session one prompt: set up Belin and build a settings page. A grader then checks the result — right imports, no hex colors, no palette classes, text styles used, build green, and the purple actually rendering on the primary button. It passed without any fixes to the docs. The measure of done was the AI no longer making mistakes, not the docs looking complete.

The lesson worth keeping.

The token bridge is small — a handful of lines. The hard part is discipline: making "what you can customise" and "what you can export" the exact same set. They had drifted apart because each component wired its own storage by hand, and the export only ever learned about the Button. One catalog that both the customiser and the export read from closed the gap, and keeps it closed — add a component in one place and it's customisable, resettable, and exportable at once.

If you already work in shadcn, that's the whole pitch. Keep your source, keep Tailwind, keep Base UI underneath. Add a role-named token layer, one bridge block, a live editor, and a catalog-driven export — and your theme becomes something you can design once and carry anywhere, including into an AI's context. How that last part works is its own story: see Give your AI a design system: the Belin Kit.

Stack: Next.js App Router, Tailwind v3, shadcn/ui on Base UI, localStorage for persistence. v3 plans: a registry endpoint that serves the theme over HTTP (installable with npx shadcn add), Supabase persistence, the Tailwind v4 migration, server-side theme injection to remove the initial flash, and Figma token sync.