Skip to content

ADR-047: Design Tokens v2 — Role-Based Semantic Naming

Accepted (amended 2026-07-05 to record the v2.1 token changes shipped 2026-06-07)

The docs/2026-06_design_system/ package introduces a “cold-minimal, dark-first” visual language. Its token sources restructure the semantic layer away from tiered names (background.primary, foreground.secondary, three-step status scales) toward role-based names that map one-to-one with intent (background, surface, foreground, muted-foreground, border, border-emphasis, link, single-token success / warning / error).

The change is structural, not cosmetic. It renames the base palettes, collapses status scales into single light/dark role tokens, adds a fontFamily token group so typography becomes swappable like colour, and cascades into global.css, ~478 utility-class usages across 40+ components and pages, the design-token test, the AI constitution, and the component showcase.

  • Intent-revealing names: bg-surface reads better than bg-background-secondary; a utility name should describe the role, not a tier number.
  • Dark mode by token, not by variant: role tokens flip in .dark via generated CSS, removing manual dark: variants from components.
  • Status colour correctness: the old three-step scales (-100/-600/-700) encouraged hardcoded tints; single role tokens + opacity utilities (bg-success/10) are leaner and consistent across light/dark.
  • Swappable typography: a fontFamily token group lets the display face change without editing component files.
  • Accessibility preserved: every gated pair must still pass the WCAG sweep in pnpm design:validate.

Option 1: Keep v1 tiered names, restyle only values

Section titled “Option 1: Keep v1 tiered names, restyle only values”

Description: Re-tune colour values inside the existing background.primary / foreground.secondary structure.

Pros:

  • No component churn
  • No test or constitution updates

Cons:

  • Locks in the weaker tier-number vocabulary
  • Status colours stay as three-step scales
  • No typography token group — display face stays hardcoded

Option 2: Role-based v2 token layer (chosen)

Section titled “Option 2: Role-based v2 token layer (chosen)”

Description: Replace tokens/base.json + tokens/semantic.json with the Path-B pair: renamed palettes (gray→slate, moonstone→violet, imperialRed→rose, orangeWeb→amber), role-based semantic names, single-token status colours, and a fontFamily group. Apply the rename map across components via a scoped codemod.

Pros:

  • Intent-revealing utilities; less manual dark-mode handling
  • Status colours become single role tokens + opacity
  • Typography swappable via tokens
  • Brand scales (primary / secondary 50–950) retained for gradients and hover states

Cons:

  • Large one-time codemod (~478 usages)
  • Status colour change is a real refactor, not a find-replace
  • Touches the AI constitution and showcase

Option 3: Run v1 and v2 token names in parallel (aliases)

Section titled “Option 3: Run v1 and v2 token names in parallel (aliases)”

Description: Emit both old and new utilities and deprecate the old gradually.

Pros: No big-bang migration

Cons: Doubles the token surface; CSS budget pressure; ambiguous source of truth; the starter is small enough to migrate atomically

Adopt the role-based v2 token layer (Option 2).

v1 (base)v2 (base)
grayslate
moonstoneviolet
imperialRedrose
orangeWebamber
Role tokenLightDark
backgroundslate-50spaceCadet
surfacewhiteslate-900
surfaceRaisedraw HSL (near-white)raw HSL (raised slate)
surfaceAccentraw HSL (violet tint)raw HSL (violet tint)
foregroundcharcoalslate-50
muted-foregroundslate-600slate-400
borderslate-200slate-800
border-emphasisslate-500slate-500
primary-foregroundwhitewhite
linkviolet-600violet-300
successgreen-700green-400
warningamber-700amber-500
errorrose-700rose-400

† Amended in v2.1 — see the amendment below. As originally accepted, success was violet-600/violet-400 (“a positive state is on-brand rather than introducing a separate green”) and there were no raised/accent surface tiers.

Amendment — v2.1 tokens (shipped 2026-06-07, recorded 2026-07-05)

Section titled “Amendment — v2.1 tokens (shipped 2026-06-07, recorded 2026-07-05)”

The design-system v2.1 merge (commit 89fc9c69) changed the semantic layer in two ways this record originally required an amendment for:

  • success moved off violet onto a dedicated green family (green-700 light / green-400 dark, with a full green 50–950 scale added to tokens/base.json). In practice the violet-success experiment failed the “status colour must read as status” test: success states were indistinguishable from primary/link emphasis.
  • Two additional surface tiers were added under the Tonal-tiers escape hatch: surfaceRaised (cards/popovers above surface) and surfaceAccent (violet-tinted emphasis panels). Both are raw HSL values rather than base-palette references.

The drift-guard test (src/__tests__/design-tokens.test.ts) pins the green mapping; this amendment is the “ADR-047 v2.1” it refers to.

Old utilityNew utility
bg-background-primarybg-background
bg-background-secondarybg-surface
text-foreground-primarytext-foreground
text-foreground-secondarytext-muted-foreground
text-foreground-subtletext-muted-foreground
border-border-primary / border-border-defaultborder-border
text-primary-600 (links)text-link
text-primary-300 (dark links)(deleted — text-link flips in dark)
bg-success-100 / text-success-700bg-success/10 / text-success (and warning/error)
bg-primary-*, text-primary-*, ring-primary-*unchanged (scale retained)

The primary and secondary colours keep their full 50–950 scales even though the role layer is flat. The signature gradient headline and button hover/active states reference specific steps (e.g. violet-400 dark / violet-600 light gradient stops, primary-300 inner-shadow highlights), so collapsing them to a single token would break those.

The canonical token file ships two surface tiers (background, surface) and two text tiers (foreground, muted-foreground). The Component Sheet illustrates a third tier; we collapse to two by default and only add muted (slate-500) or a raised surface token if a visual-diff regression requires it. Any such addition is recorded as an amendment here. (Exercised by the v2.1 amendment above: surfaceRaised and surfaceAccent.)

  • Intent-revealing utilities; fewer manual dark: variants
  • Status colours unified as role token + opacity
  • Typography swappable via the fontFamily token group
  • One source of truth — no parallel alias layer
  • One-time ~478-usage codemod plus a hand-reviewed status refactor
  • AI constitution examples and the showcase need updating
  • The token build pipeline (scripts/src/build-tokens.ts) is unchanged — it already resolves nested scales and single role tokens with dark references, and flattens the new fontFamily group to --font-family-display / --font-family-text
  • Token build: pnpm tokens:build emits the new --color-* and --font-family-* vars
  • Contrast: pnpm design:validate passes every gated pair in both modes
  • Tests: src/__tests__/design-tokens.test.ts asserts the role set, source mappings, retained brand scales, and Geist-first display family
  • No dead classes: pnpm build + a light/dark visual diff of /showcase shows no unstyled utilities
  • CSS budget: total CSS stays < 50KB (pnpm perf:budgets)

The canonical Style-Dictionary source is the wiring/base.json + wiring/semantic.json pair (palettes slate/violet/rose/amber, {value} token wrappers). The sibling tokens/base.json is a flat prototype the Visual Language HTML fetches — it uses the old palette names (gray/moonstone/imperialRed/orangeWeb) and is reference/intent only. Only the wiring/* pair is copied into tokens/base.json + tokens/semantic.json and wired into the build.

  • Testable consequences:
    • TC-1: every gated token pair passes the WCAG contrast sweep.
    • TC-2: no dark: variant utilities appear in src/ — role tokens flip in .dark via generated CSS.
    • TC-3: no hardcoded colour literals (hex/rgb/hsl) appear outside tokens/ and generated token CSS.
  • Checks:
    • TC-1 → design:validate in CI (status: block, pre-existing gate)
    • TC-2 → check no-dark-variants (status: warn)
    • TC-3 → check no-hardcoded-colors (status: warn)
  • Not machine-checkable: whether a token name is intent-revealing is a naming judgment.
  • Graduation log: (empty at creation; entries added when a check changes status)

Date: 2026-06-06
Participants: Chris Pezza, template maintainers
Outcome: Accepted