Skip to content

ADR-029: Role-Based Design Tokens

Date: 2026-07-12

Accepted

The view layer had two competing color systems: a static @theme block in input.css with -dark-suffixed twins (--color-background-dark) driving manual dark: variants in every component, and a runtime-flipping semantic-colors.css loaded twice (once via @import, once as a separate <link>). On top of both, components hardcoded Tailwind grays (text-gray-500 dark:text-gray-400, ~120 usages), the primary scale was fake (500 and 600 were the same hex), and the demo design brief explicitly flagged promoting the palette roles into a formal token layer mirroring the astro starter’s ADR-047.

Adopt role-based semantic tokens as the single styling vocabulary, mirroring astro-performance-starter ADR-047 adapted to this palette (teal, bittersweet, night, nyanza, sage):

RoleLightDark
backgroundnyanzanight
surfacewhite#1a1a1a
surface-hovergray-100#262626
foregroundnightnyanza
muted-foregroundgray-600gray-400
bordergray-200#2e2e2e
primary / primary-strongteal / dark teal(constant)
accentsagesage
linkdark tealsoft teal
dangerbittersweetsoft bittersweet
successgreen-700green-400
warningamber-700amber-500

Rules with constitutional force (ADR-018 layering):

  1. Dark mode flips tokens, not utilities. The .dark block in input.css overrides the role variables; components never write dark: color variants. (dark: remains legal for non-color concerns, e.g. x-cloak interplay, but no color role may fork per mode in a component.)
  2. No raw grays in components. text-muted-foreground, border-border, bg-surface-hover, bg-border (skeletons) replace the gray-N00 family.
  3. Solid buttons ride brand constants (.btn-primary/.btn-danger keep teal/bittersweet with white text in both modes); text and tints ride roles (text-danger, bg-success/10) and flip for contrast.
  4. Status feedback is tint + role text (bg-danger/10 text-danger), not solid status backgrounds with white text — the astro v2.1 lesson.
  5. semantic-colors.css is deleted; input.css is the single token source.

A tokens_test.go scan enforces rules 1–2 over internal/view/**/*.templ so drift fails task ci (ADR-021).

  • One source of truth; the double-loaded stylesheet and the fake primary scale are gone; components read as intent (text-muted-foreground), not as palette trivia.
  • One-time sweep of ~15 templ files (~230 class edits) — mechanical, guarded by the visual pass and the existing markup tests.
  • The old text role is renamed foreground (astro parity); -dark token twins are deleted.
  • Contrast obligations move into the token layer: dark-mode link, danger, success values are lightened variants chosen for the night background.
  • Keep both systems, fix values only. Rejected — the duplication is the bug; every new component would keep choosing between vocabularies.
  • Content-hashed design-token pipeline (Style Dictionary) like astro. Rejected for now — this starter has no token build step and doesn’t need one at this scale; the astro mapping is captured directly in CSS.
  • astro-performance-starter ADR-047 (role-based naming, v2.1 status-color amendment) — the sibling-template precedent this adapts.
  • ADR-007, ADR-017, ADR-024 (design brief flagged the gap).
  • Testable consequences:
    • TC-1: No dark: color variants in internal/view/**/*.templ.
    • TC-2: No raw gray-N00 utilities in components.
  • Checks:
    • TC-1, TC-2 → internal/view/tokens_test.go via task ci (status: block, pre-existing)
  • Not machine-checkable: Rules 3–4 (brand constants on solid buttons; status feedback as tint + role text) — semantic composition judgment.
  • Graduation log:
    • 2026-08-19 — TC-2 widened (#85): the tokens_test.go scan now rejects raw utilities from every Tailwind palette family (slate|gray|zinc|…|green|blue|…-N00), not only gray — the original rule let a bg-green-100 success box and two bg-blue-600 CTAs survive as pre-ADR leftovers. Status unchanged (block, pre-existing); the widened rule shipped with the four leftovers fixed, so it landed green.