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.

Tokens come in two layers and the distinction matters for who edits what:

  • Palette tokens are brand-specific. Colours like --primary, --background, and --border change per theme; every brand kit ships its own values.
  • Component-layer tokens are design-system constants. Sizes and typography like --button-radius and --font-sans are the same across every theme. They reference palette tokens with var(--...) 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.

TokenUsed for
--primary / --primary-foregroundBrand action color and the legible text colour that sits on top of it
--accent / --accent-foregroundSecondary brand accent for highlights and emphasis
--secondary / --secondary-foregroundQuiet surfaces (chips, secondary buttons)
--background / --foregroundPage background and default text
--muted / --muted-foreground / --muted-backgroundQuiet text and faint surface tints
--card / --card-foregroundCard surface and card text
--popover / --popover-foregroundPopovers, menus, tooltips
--border / --input / --ringBorders, input frames, focus rings
--destructive / --destructive-foregroundDanger 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.

TokenUsed for
--font-sans / --font-serifTypography stacks; overridden per brand
--button-radiusButton corner radius; defaults to var(--radius-4xl)
--card-radiusCard corner radius
--input-borderInput 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.

  • Themes — browse the themes shipped with this registry and switch between them live.
  • Brand kits — package a palette into a reusable brand identity.