Theming

Three layers: primitives hold the raw palette, semantic tokens name a role, and components only ever reference the roles.

The three layers

Primitives are the palette — --color-blue-400, --color-neutral-950. Semantic tokens name what a color is for — --color-text-primary, --color-background-full — and point at a primitive. Components reference only the semantic layer.

That indirection is what makes theming cheap: change what a role points at and every component follows, in both light and dark, with no component edits.

Changing the accent

Interactive surfaces — the primary button gradient, ghost and link buttons, checkbox, radio, switch, slider, tabs, sidebar selection, and the focus ring — reference --color-accent-* rather than a raw hue. Override those eleven variables and the whole system re-tints.

app/globals.css
/* After importing BoardCN's globals.css */
:root {
  --color-accent-50:  var(--color-violet-50);
  --color-accent-100: var(--color-violet-100);
  --color-accent-200: var(--color-violet-200);
  --color-accent-300: var(--color-violet-300);
  --color-accent-400: var(--color-violet-400);
  --color-accent-500: var(--color-violet-500);
  --color-accent-600: var(--color-violet-600);
  --color-accent-700: var(--color-violet-700);
  --color-accent-800: var(--color-violet-800);
  --color-accent-900: var(--color-violet-900);
  --color-accent-950: var(--color-violet-950);
}
/* After importing BoardCN's globals.css */
:root {
  --color-accent-50:  var(--color-violet-50);
  --color-accent-100: var(--color-violet-100);
  --color-accent-200: var(--color-violet-200);
  --color-accent-300: var(--color-violet-300);
  --color-accent-400: var(--color-violet-400);
  --color-accent-500: var(--color-violet-500);
  --color-accent-600: var(--color-violet-600);
  --color-accent-700: var(--color-violet-700);
  --color-accent-800: var(--color-violet-800);
  --color-accent-900: var(--color-violet-900);
  --color-accent-950: var(--color-violet-950);
}

Overriding a single role

Any semantic token can be redefined the same way. Because @theme inlinere-exports each one, Tailwind reads the live value — so an override applies at runtime without a rebuild.

app/globals.css
.dark {
  /* Warm the dark canvas without touching a single component. */
  --color-background-full: oklch(0.18 0.01 60);
}
.dark {
  /* Warm the dark canvas without touching a single component. */
  --color-background-full: oklch(0.18 0.01 60);
}

The Colors page lists every token, generated from the stylesheet itself.

Typography

The ramp is defined as composite utilities: text-body-medium carries size, line height, letter spacing, and weight together. To change the family, override the font stack once — the ramp inherits it. See Typography for the full scale.

A note on cx

BoardCN's cx helper wraps tailwind-merge and is taught about every composite text utility. If you add your own text-* style via @theme, register it in utils/cx.ts too — otherwise tailwind-merge reads it as a text-color utility and will drop your color classes.