The foundation is token-driven. Colors are CSS variables mapped to Tailwind utilities; spacing and radius are compiled from a TypeScript token scale. Nothing in a component hardcodes a hex value or a raw utility.
Color tokens
Colors follow the shadcn semantic-pairs contract: every surface token has a matching foreground token, declared as CSS variables on :root and bridged into Tailwind v4 through @theme inline:
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.4386 0 0);
}The pairs the primitives consume:
| Pair | Consumed by |
|---|---|
background / foreground | Shell, Section, Box base surfaces |
card / card-foreground | Box surface="card" |
muted / muted-foreground | Section surface="muted", Box |
primary / primary-foreground | Section surface="primary", Box |
secondary / secondary-foreground | Section surface="secondary", Box |
To re-theme, re-declare the variables in your own stylesheet after the base theme — components pick up the new values without any code changes. Inversion is consumption, not configuration: Section surface="inverted" renders bg-foreground text-background, so one palette powers both directions.
Spacing
Spacing is a TypeScript scale, not CSS variables. lib/tokens.ts defines the steps "0", "0.5", "1", "2", "2.5", "3", "4", "5", "6", "7", "8", "9", "10", "12", "14", "16", "20", "24" — the Tailwind spacing scale — and generates the class maps the layout family spreads into its variants:
export const gapClasses = spacingClasses("gap");
// { "0": "gap-0", "0.5": "gap-0.5", ..., "12": "gap-12", ..., "24": "gap-24" }When you write <Stack gap="12">, the prop keys the map and the component renders gap-12. One step equals 4 pixels; sizeTokens exposes the pixel values for layout math. Because the keys are Tailwind step names, no arbitrary-value utilities are generated and the compiled CSS stays static.
Radius
One base variable derives the whole scale by offset, so changing a single value re-scales every radius utility:
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}Box radius accepts the compiled keys: "none", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl".
Class discovery
Tailwind v4 scans source files to know which utilities exist. The token-generated classes are composed at build time inside lib/tokens.ts, so make sure your content configuration sees that file:
@source "../**/*.{ts,tsx}";If your scanner cannot see the module (for example, you consume a built dist), tokens.ts also exports _safelist: a space-joined string of every generated utility for @source inline(...).
Dark mode
All color tokens are CSS variables. To support dark mode, re-declare them under a .dark class (and Tailwind's dark variant of your choice) — borders and overlays usually work best as alpha whites in dark. Wiring a theme switcher is up to your application; the registry ships tokens, not a provider.