Status: active · Authoritative reference for the theme-aware Tailwind tokens used by the React frontend.
The frontend is fully themeable: every color flows through CSS custom properties, so a new theme is created by editing variable values in one file — never component classes. This doc is the contract that makes that true. Use only the tokens listed here; they are the complete set.
- Token utilities + mappings:
frontend/src/assets/styles/tailwind-theme.css(the@themeblock) - Light / dark / contrast values:
frontend/src/assets/styles/themes.css - Entry import:
frontend/src/index.css. Tailwind v4, CSS-first (notailwind.config.jscolors). - Dark mode =
darkclass on<html>.
theme-primary, theme-secondary, theme-tertiary, theme-quaternary are TEXT colors
(--color-theme-primary = --color-text-primary). In light mode primary text is near-black; in
dark mode it is near-white. Tailwind still generates bg-/border-/ring- utilities from
them, so:
<div className="bg-theme-primary">…</div> {/* ❌ paints a TEXT color as a background */}renders a near-white background in dark mode — and if its text is also text-theme-primary,
you get white-on-white / invisible content. This is the single most common theme bug.
| ❌ Never | ✅ Use instead | For |
|---|---|---|
bg-theme-primary |
bg-theme-interactive-primary (accent) or bg-theme-surface (panel) |
button vs card |
bg-theme-secondary / -tertiary |
bg-theme-surface-hover / bg-theme-surface-secondary |
subtle fills |
border-theme-primary |
border-theme / border-theme-strong / border-theme-focus |
borders |
ring-theme-primary |
ring-theme-interactive-primary / ring-theme-focus |
focus rings |
hover:bg-theme-primary-hover |
hover:bg-theme-interactive-primary-hover |
this token does not exist (inert) |
Rule of thumb: theme-primary/secondary/tertiary/quaternary → only ever text-…. For a
background you want a surface, a background, an interactive, or a semantic token.
| Token | Use |
|---|---|
text-theme-primary |
Headings, body, primary content |
text-theme-secondary |
Secondary content (still WCAG-AA, 4.5:1) |
text-theme-tertiary |
Captions/metadata only (3:1 — not for body text) |
text-theme-quaternary |
Faintest hints |
text-theme-on-primary |
Text/icons on an interactive-primary background |
text-theme-inverse |
Text on an inverted surface |
text-theme-link / text-theme-link-hover |
Hyperlinks |
| Token | Use |
|---|---|
bg-theme-background |
Page / app shell background |
bg-theme-background-secondary / -tertiary |
Nested zones, sidebars |
bg-theme-background-elevated / -muted |
Raised / de-emphasized zones |
bg-theme-surface |
Card / panel / table body |
bg-theme-surface-hover |
Hover state, subtle header/cell fill |
bg-theme-surface-selected / -pressed |
Selected / pressed states |
bg-theme-surface-secondary |
Alternate/striped surface |
bg-theme-surface-disabled |
Disabled control surface |
| Token | Use |
|---|---|
bg-theme-interactive-primary (+ -hover / -active / -disabled) |
Primary buttons, toggles, selected chips, accent fills |
bg-theme-interactive-secondary (+ -hover / -active / -disabled) |
Secondary buttons |
For success, warning, error, danger, info:
| Slot | Token | Use |
|---|---|---|
| Foreground | text-theme-success-fg |
Status text / icon (saturated, readable) |
| Background | bg-theme-success-bg |
Status pill / banner fill (tint) |
| Border | border-theme-success-border |
Status outline |
<span className="bg-theme-success-bg text-theme-success-fg border border-theme-success-border px-2 py-0.5 rounded">Active</span>The bare
text-theme-success(no-fg) resolves to the same value as-fg, but prefer the explicit-fg/-bg/-bordertriad — it documents intent and is what the lint guard expects.
border-theme (standard) · border-theme-light · border-theme-strong · border-theme-focus
· border-theme-{success,warning,error,danger,info}.
ring-theme-focus · ring-theme-interactive-primary. Never ring-theme-primary.
// Card / panel
<div className="bg-theme-surface border border-theme rounded-lg shadow-sm">
<h3 className="text-theme-primary font-semibold">Title</h3>
<p className="text-theme-secondary">Description</p>
</div>
// Primary button
<button className="bg-theme-interactive-primary text-theme-on-primary hover:bg-theme-interactive-primary-hover rounded px-3 py-2">
Submit
</button>
// Secondary button
<button className="bg-theme-interactive-secondary text-theme-primary hover:bg-theme-interactive-secondary-hover rounded px-3 py-2">
Cancel
</button>
// Table
<table className="min-w-full divide-y divide-theme">
<thead className="bg-theme-surface-hover">
<tr><th className="text-theme-secondary text-left p-3">Name</th></tr>
</thead>
<tbody className="bg-theme-surface divide-y divide-theme">
<tr className="hover:bg-theme-surface-hover">
<td className="text-theme-primary p-3">Item</td>
</tr>
</tbody>
</table>The standard
<Button>/<Badge>UI components already encode these tokens — prefer them over hand-rolled buttons/badges.
Three layers, each referencing the one below:
component className → @theme token → semantic var → base scale
bg-theme-surface --color-theme-surface --color-surface --color-neutral-50
text-theme-primary --color-theme-primary --color-text-primary --color-neutral-900
- To re-theme: edit only the semantic var values in
themes.css(the:root/ light block ~line 284, the dark block ~line 359, plusprefers-contrast/forced-colors). Components and@themetokens never change. This is the entire surface area of a theme. - To add a brand color: add a base scale + point the semantic vars at it.
- Never hardcode hex or Tailwind palette colors (
bg-white,text-gray-700,bg-red-500) in components — thehardcoded-color-check.shhook rejects them. Only exception:text-whiteon a guaranteed-dark colored background.
- Edit-time hook:
.claude/hooks/theme-token-check.shflags text-token-as-background/border/ring (bg-/border-/ring-theme-{primary,secondary,tertiary,quaternary}) and the inert*-theme-primary-hovertoken. - Adherence scan:
scripts/pattern-validation.sh→ "Forbidden text-token-as-background". - Hardcoded colors:
.claude/hooks/hardcoded-color-check.sh+./scripts/fix-hardcoded-colors.sh.
- ../guides/frontend.md — component patterns
- ../guides/accessibility.md — contrast, focus rings
Last rewritten 2026-06-28 against the live @theme inventory (corrected phantom tokens + the
bg-theme-primary button footgun that this doc previously taught).