Theming & design tokens
The Showcase design system is themed entirely through CSS custom properties. Components never hard-code colours, radii, or typography — they reference tokens, and a theme is just a set of token values. Swapping themes is a stylesheet swap, not a rebuild.
src/app/_lib/themes/tokens.ts. Both the static theme generator and the orchestrator-served themes validate against it, so missing or unknown tokens fail loudly instead of silently rendering off-brand.Tokens come in two layers and the distinction matters for who edits what:
- Palette tokens are brand-specific. Colours like
--primary,--background, and--borderchange per theme; every brand kit ships its own values. - Component-layer tokens are design-system constants. Sizes and typography like
--button-radiusand--font-sansare the same across every theme. They reference palette tokens withvar(--...)so a recoloured brand still gets the right shape.
Every theme defines these. Pair tokens (e.g. --primary with --primary-foreground) give each surface its legible text colour.
| Token | Used for |
|---|---|
--primary / --primary-foreground | Brand action color and the legible text colour that sits on top of it |
--accent / --accent-foreground | Secondary brand accent for highlights and emphasis |
--secondary / --secondary-foreground | Quiet surfaces (chips, secondary buttons) |
--background / --foreground | Page background and default text |
--muted / --muted-foreground / --muted-background | Quiet text and faint surface tints |
--card / --card-foreground | Card surface and card text |
--popover / --popover-foreground | Popovers, menus, tooltips |
--border / --input / --ring | Borders, input frames, focus rings |
--destructive / --destructive-foreground | Danger surfaces and the legible text on them |
// A primitive consumes palette tokens through Tailwind utilities.
// "bg-primary" resolves to var(--primary), "text-foreground" to
// var(--foreground), and so on.
<Button className="bg-primary text-primary-foreground">
Save
</Button>These rarely change per brand. Override them only when you want the whole system to use different radii or typography.
| Token | Used for |
|---|---|
--font-sans / --font-serif | Typography stacks; overridden per brand |
--button-radius | Button corner radius; defaults to var(--radius-4xl) |
--card-radius | Card corner radius |
--input-border | Input border colour; references --color-border |
:root {
/* component-layer constants reference palette via var(--...) */
--card-radius: 0.75rem;
--button-radius: var(--radius-4xl);
--input-border: var(--color-border);
}Tailwind v4 reads its design tokens from an @theme block. The block has to live inline in your entry stylesheet, alongside the @import "tailwindcss" line — pushing it behind another @import chain breaks token detection in this setup.
/* src/app/globals.css — must be inline, not behind an @import */
@import "tailwindcss";
@theme {
--color-primary: var(--primary);
--color-foreground: var(--foreground);
--color-background: var(--background);
--font-sans: var(--font-sans);
/* …rest of the bridge */
}Inside @theme, each entry (like --color-primary) bridges a Tailwind utility (bg-primary) to a CSS variable (--primary). The palette CSS variable is what brand themes redefine.
A theme is one stylesheet that redefines the palette tokens on :root. The static generator writes one of these per brand to public/themes/<slug>.css.
/* public/themes/<brand-slug>.css — generated, not hand-edited */
:root {
--primary: oklch(0.62 0.18 248);
--accent: oklch(0.78 0.14 196);
--background: oklch(0.99 0 0);
--foreground: oklch(0.22 0.02 248);
/* …all palette tokens defined */
}Component-layer tokens stay untouched, so the only thing that changes between brands is colour and typography. Shape and density stay consistent.
--accent or --secondary rather than adding --banner-accent. New tokens are a system-level change, not a one-off.- Themes — browse the themes shipped with this registry and switch between them live.
- Brand kits — package a palette into a reusable brand identity.