Aller au contenu

Design tokens

Status: Implemented Last reviewed: 2026-04-18 Source of truth: static/css/base.css lines 27-340 (@layer base { :root { ... } } + theme overrides)

Scope

Catalogue of every CSS custom property (var(--*)) defined in the design system. New templates and CSS must pick from these tokens — never hardcode colors, sizes, shadows, radii, or motion timings.

This file is enumerative. Pick a token, copy the name, use it. If you can't find a token that fits, the answer is to add one to base.css and document it here, not to hardcode a value.

The hard rule

Never write a literal #hex, rgb(…), rgba(…), oklch(…), or hardcoded px / rem value in a template style= attribute or component CSS. The only place literal values live is static/css/base.css (token definitions) and Bootstrap-overriding selectors that consume --bs-* variables.

Two narrow exceptions: - Border widths (1px, 2px) — there is no --border-width-* token yet; literal 1px is acceptable until one is added. - 00, 0px, 0rem, none are always fine.

Anything else: use a token, or add one.

How to pick a token

Decision order — start at the most semantic, fall back only when nothing fits:

  1. Semantic surface tokens (--bg-*, --text-*, --border-*) — these auto-adapt to light/dark mode. Default choice for component backgrounds, text, and borders.
  2. Semantic feature tokens (--focus-ring-*, --shadow-*, --radius-*, --space-*, --font-size-*, --duration-*, --ease-*) — for non-color properties.
  3. Brand-aliased colors (--brand-*) — for accent / interactive / branded surfaces. Auto-resolves to teal (default) or blue when [data-brand="blue"] is set on an ancestor.
  4. Status palettes (--success-*, --warning-*, --error-*, --info-*) — for status-bound visuals (charts, alerts, validation messages).
  5. Direct color primitives (--neutral-*, --teal-*, --blue-*, --accent-*) — last resort. If you reach for these, ask first whether a semantic token should exist instead.

If you find yourself writing var(--neutral-200) for a border, you should have written var(--border-primary). The semantic tokens are the contract; the primitives are the implementation.

Catalogue

Typography (--font-size-*)

Token Value Use for
--font-size-xs 0.6875rem (11px) counters, timestamps
--font-size-sm 0.75rem (12px) badges, table headers
--font-size-base 0.8125rem (13px) body, tables, sidebar
--font-size-md 0.875rem (14px) card headers, emphasis
--font-size-lg 1rem (16px) section titles
--font-size-xl 1.125rem (18px) page titles
--font-size-2xl 1.25rem (20px) main headings
--font-size-3xl 1.5rem (24px) display, KPI secondary
--font-size-4xl 1.75rem (28px) display, KPI primary

Spacing (--space-*)

--space-0 (0) · --space-1 (4px) · --space-2 (8px) · --space-3 (12px) · --space-4 (16px) · --space-5 (20px) · --space-6 (24px) · --space-8 (32px) · --space-10 (40px) · --space-12 (48px) · --space-16 (64px) · --space-20 (80px) · --space-24 (96px).

Use for margin, padding, gap, and any layout dimension. Never write a literal margin: 12px — write margin: var(--space-3).

Radii (--radius-*)

Token Value Use for
--radius-sm 6px small elements, badges
--radius-md 8px buttons, inputs, default
--radius-lg 12px cards, modals
--radius-xl 16px large features
--radius-2xl 20px hero sections
--radius-full 9999px pills, avatars

Shadows (--shadow-*)

--shadow-xs · --shadow-sm · --shadow-md · --shadow-lg · --shadow-xl · --shadow-2xl. Use exclusively — never write box-shadow: 0 1px 2px rgba(...) directly. Cards typically use --shadow-sm, modals --shadow-lg or --shadow-xl.

Motion (--duration-*, --ease-*)

Durations: --duration-instant (100ms) · --duration-fast (150ms) · --duration-base (200ms) · --duration-slow (300ms) · --duration-slower (500ms).

Easing: --ease-in-out (default) · --ease-out · --ease-in · --ease-bounce.

Compose: transition: opacity var(--duration-fast) var(--ease-out).

Focus

--focus-ring-color (defaults to var(--brand-600)) · --focus-ring-width (3px) · --focus-ring-offset (2px) · --focus-ring-shadow · --focus-ring-alpha.

Custom focus styles must compose these. Never hand-roll a focus outline color.

Breakpoints / containers

--breakpoint-sm (640px) · --breakpoint-md (768px) · --breakpoint-lg (1024px) · --breakpoint-xl (1280px) · --breakpoint-2xl (1536px). Mirrored as --container-* for max-width. Use these in @media queries instead of literals.

Color primitives — neutral, brand families, status

Neutral (text, surfaces, borders): --neutral-50--neutral-900 — 9-step grayscale.

Brand families (each 50–950, 11 steps): - --teal-* — default brand (oklch). - --blue-* — alternate brand ([data-brand="blue"]) (oklch). - --accent-* — amber/orange accents (hex).

Status families (each 50–900, 9 steps): --success-*, --warning-*, --error-*, --info-* — all hex.

Hex aliases for chart libs that won't accept oklch: --teal-600-hex (#0d9488), --blue-600-hex (#2563eb).

Brand-aliased semantic colors (--brand-*)

--brand-50--brand-950 resolve to teal by default, blue when [data-brand="blue"] is on an ancestor. Use these when you want the accent colour to follow the user's brand pick — for buttons, links, focused inputs, branded chips.

Chart palettes

  • Sequential / categorical: --chart-color-1--chart-color-6. --chart-color-1 re-aliases to the brand hex automatically.
  • Named extras: --chart-cyan-600, --chart-sky-600, --chart-indigo-500, --chart-violet-500, --chart-rose-500, --chart-amber-500.
  • Dashboard slots: --chart-1--chart-8 (first three follow brand/info/warning, rest are fixed picks).

Semantic surface tokens — the default for most components

These are the tokens you will use 90% of the time. They auto-flip in dark mode without any extra work.

Token Light value Dark value Use for
--bg-primary --neutral-50 --neutral-900 page background
--bg-secondary white --neutral-800 section / panel surface
--bg-tertiary --neutral-100 --neutral-700 subtle alt-row, code blocks
--bg-card white --neutral-800 every card body
--bg-card-header --neutral-100 --neutral-700 card-header background
--bg-interactive --brand-50 --brand-900 hover/active brand surfaces
--bg-interactive-hover --brand-100 --brand-800 hover state on interactive
--text-primary --neutral-800 --neutral-100 body text
--text-secondary --neutral-600 --neutral-300 secondary labels, meta
--text-tertiary --neutral-500 --neutral-400 hints, disabled, captions
--text-on-brand white --neutral-950 text on a brand-coloured surface
--text-on-brand-muted oklch(1 0 0 / 0.8) oklch(0 0 0 / 0.8) secondary text on brand
--border-primary --neutral-200 --neutral-700 default 1px borders
--border-secondary --neutral-300 --neutral-600 stronger separators

Brand and theme switching

Theme is controlled by data-theme on <html> (or any ancestor): - data-theme="auto" (default) — follows prefers-color-scheme: dark. - data-theme="light" — force light. - data-theme="dark" — force dark.

Brand is controlled by data-brand on the same element: - (none) — teal (default). - data-brand="blue" — blue accent across --brand-* aliases.

Implication for components: if you only use semantic surface tokens (--bg-*, --text-*, --border-*, --brand-*), you get dark mode and brand switching for free. If you reach for primitive colours (--teal-600, --neutral-200), you bypass the theming system.

Common substitutions (audit cheatsheet)

The 2026-04 audit (see roadmap/backlog/ui-design-system-mechanical-fixes-2026-04.md) found ~52 hardcoded values. Map each to the right token:

Hardcoded Replace with
#0d9488 var(--teal-600-hex) if a hex string is required (chart libs); var(--brand-600) for CSS
rgba(0, 0, 0, 0.05) etc. one of --shadow-xs/sm/md/lg/xl/2xl
80px (avatar/photo size) no token exists yet — propose adding --size-avatar-md; otherwise inline 1px style is acceptable
20px (progress bar height) propose --size-progress-height; or use --space-5 if visually identical
18px, 10px, 3px (icon/spacing) nearest --space-* token (--space-4/--space-2/--space-1) — pick the closest, don't introduce new pixel values
#fff, white for backgrounds var(--bg-card) or var(--bg-secondary) (so dark mode works)
#000, black for text var(--text-primary)
Any literal grey #666 / #999 / etc. one of --text-secondary / --text-tertiary / --neutral-*
Bootstrap bg-success / bg-warning etc. on badges badge-status-active / -pending / -inactive / -neutral (see ui/badges.md)

Known deviations

  • apps/crm/templates/crm/prospect_detail.html:540 — uses #0d9488 and rgba(0,0,0,0.3) inline. Tracked in the 2026-04 audit backlog.
  • apps/dentists/templates/dentists/dentist_detail.html:1780px photo sizing. No avatar-size token exists yet; deferred until the catalogue grows or this becomes a recurring pattern (currently single-occurrence).
  • apps/finance_workflow/templates/finance_workflow/checklist_list.html:5920px progress bar height. Same: no progress-size token; defer.
  • apps/register/templates/register/assembly_detail.html:158 — fallback hex inside a var(). Strip the fallback; the variable is always defined.
  • static/css/register.css, static/css/captable.css — pixel literals for icons / micro-spacing. Replace with --space-* substitutions per the cheatsheet.

Once the audit backlog item lands, this section should empty out. Re-running the design-tokens audit subagent should then return zero violations.

Adding a new token

If you genuinely need a value the catalogue doesn't cover:

  1. Decide which group it belongs to (spacing? colour? motion?). If it doesn't fit any, you probably shouldn't add it.
  2. Add the definition to static/css/base.css inside the matching group, with a comment naming the use case.
  3. If it's a colour or surface, add the dark-mode override in both [data-theme="dark"] and @media (prefers-color-scheme: dark) [data-theme="auto"] blocks.
  4. Add a row to the relevant table in this file.
  5. Bump Last reviewed at the top.

Never add a token "just in case." A token whose only consumer is one component is a hardcoded value with extra steps.