Zum Inhalt springen

ADR-0006: Design system — tokens + Tailwind v4 + own-the-code primitives

ADR-0006: Design system — tokens + Tailwind v4 + own-the-code primitives

Section titled “ADR-0006: Design system — tokens + Tailwind v4 + own-the-code primitives”
  • Status: Accepted (2026-05-31)
  • Deciders: @ArneFfm
  • Master design impact: Adds D41: tokens + Tailwind v4 + own-the-code primitives, islands renderer Preact + Ark UI. Makes D17 (Astro + islands) and D18 (Starlight) concrete; contradicts no existing D-row (D1–D40).
  • Backing research: research/design-system-research.md
  • Implementation spec: docs/superpowers/specs/2026-05-23-14-design-system-design.md
  • Implementation plan: docs/superpowers/plans/2026-05-31-design-system.md

No design system is planned anywhere in the 13 specs or the master decision log. The consequence is visible in the code: apps/web ships with zero styling. grep '<style' across apps/web/src returns nothing; components such as SpotlightCard.astro and AppShell.astro emit class hooks (spotlight, app-rail, app-topbar) that no stylesheet defines, so the public site and dashboard currently render as unstyled HTML. There is no Tailwind, no UI framework integration, and no token layer in apps/web.

Meanwhile three other surfaces have their own (or future) look: the Starlight docs site (docs-site/) defines brand values in src/styles/brand.css (--sl-color-*); the OG-image generator (apps/web/src/lib/og-svg.ts) hardcodes brand colors/type; and the admin console (spec 10) has no frontend yet. Four surfaces, no shared source of brand truth → guaranteed drift.

The framing question from the request: is “just apply Tailwind” right, or is a template / component library / other design system smarter?

Constraints: Cloudflare Workers SSR (output: 'server', prefer zero-runtime CSS); Lighthouse is a required CI gate (minimal CSS/JS); 100% coverage + zero warnings (pnpm verify, spec 13) makes opaque vendored runtime code unviable; monorepo packages/* convention; Paraglide per-call/no-global-state isolate rule; existing a11y attributes must be preserved.

Adopt a three-layer design system:

  1. packages/design-tokens — the single source of truth, emitting CSS custom properties (--nyl-*: color, type, space, radius, shadow, motion, z-index, breakpoints). Consumed by all four surfaces: Tailwind @theme, Starlight --sl-* overrides, the OG SVG generator, and any raw CSS. CSS variables (not a JS token object) cross every boundary, support runtime theme swap (data-theme), and carry no module-level mutable state (isolate-safe, consistent with the Paraglide rule). A W3C-design-tokens JSON + Style Dictionary source is optional and deferred.

  2. Tailwind v4 via @tailwindcss/postcss (CSS-first @theme reading the token variables; the deprecated @astrojs/tailwind integration is not used). Zero-runtime, purged utilities → Lighthouse-friendly. Transport note: the @tailwindcss/vite plugin is currently broken on Astro 6’s rolldown-bundled Vite (withastro/astro#16542, closed — resolved by switching to PostCSS, not a plugin fix), so the PostCSS transport + postcss.config.mjs is the chosen path. The CSS-first @theme model is identical either way. Token var() mappings use @theme inline so dark-mode token swaps are not shadowed.

  3. packages/ui — shadcn-style own-the-code primitives, split by JS need: presentational primitives as Astro components (zero JS) for the public site + dashboard (Button, Card, Badge, Input, Alert; finally defining the spotlight*/app-* hooks), and interactive primitives as islands on a headless WAI-ARIA library (Dialog, Tabs, Combobox, Calendar, Toast), hydrated only where used. We vendor and test the source rather than depend on an npm component library, because only owned code can meet the 100%-coverage bar and avoid lock-in.

Token contract. Tokens are named --nyl-<category>-<name> and are semantic, not raw (e.g. --nyl-color-surface, not --nyl-gray-900), so dark mode is a remap of one [data-theme="dark"] block rather than a rewrite of every consumer. Categories: color, font, text (size), space, radius, shadow, motion/ease, z, breakpoint. Tailwind @theme and Starlight --sl-* both alias these; no consumer hardcodes a hex value. The exact token set is frozen in the implementation spec.

Accessibility & testing are acceptance criteria, not follow-ups. Every primitive ships with (a) a Vitest test asserting rendered markup/variants and (b) for interactive islands, an assertion of the WAI-ARIA contract (role, aria-*, focus/keyboard) — this is how the 100%-coverage bar is met on owned code. The public site must not regress its Lighthouse a11y/perf budget.

Islands renderer: Preact + Ark UI (ratified). Layer 3’s interactive primitives are built on @astrojs/preact (compat: true) for the smallest island runtime (~4 kB) — the Lighthouse-friendliest choice — with the React ecosystem reachable via preact/compat. Ark UI (Zag.js-based, WAI-ARIA headless) supplies Dialog/Tabs/Combobox/DatePicker/Toast behaviour; it ships as @ark-ui/react (there is no @ark-ui/preact) and runs under Preact through the compat alias — a supported-but-not-first-class path that may need react→preact/compat aliases in tsconfig and the test config. Solid + Kobalte

  • shadcn-solid was the considered alternative (first-class Solid primitives, no compat shim) and is recorded below. Layers 1–2 (tokens + Tailwind) are renderer-agnostic and unaffected by this choice.
OptionWhy rejected
Plain Tailwind onlySolves utilities, not accessible behaviour (Dialog/Combobox/Calendar focus + keyboard). Necessary but insufficient — kept as Layer 2, not the whole answer.
daisyUIZero-JS and Astro-friendly, but themes via its own semantic-class + CSS-var namespace, competing with the token package and Starlight --sl-* → two theming systems to keep in sync (the drift failure ADR-0001 warns against). CSS-only widgets lack real focus management.
Flowbite / Preline / MUI / MantineLarge JS/CSS bundles vs the Lighthouse gate; MUI/Mantine are React+Emotion CSS-in-JS (runtime tax, wrong for Workers SSR); cannot reach 100% coverage on vendored runtime code.
Pre-built template (astro.build/themes)Great bootstrap/visual reference, but a snapshot, not a system: no token package, no packages/ui boundary, no tests, hardcoded colors, no Starlight/OG link. Mine it for markup, then refactor into the three layers.
Panda CSS (runner-up)Principled zero-runtime token+recipe engine, but Tailwind v4 @theme already makes the token package the native config, and Starlight/shadcn/Astro ecosystem assume Tailwind. Kept on the shelf as the fallback.
JS token objectDoesn’t cross the CSS/Starlight/OG-SVG boundaries for free; introduces module state. CSS custom properties chosen instead.
npm component dependency (any)Fails the 100%-coverage + zero-warning quality bar and adds version-churn lock-in. Own-the-code chosen.
  • D17 / D18 — Astro + islands and Starlight remain; this ADR styles them and unifies their brand via the token package, changing no routing or deploy shape.
  • Paraglide i18n — components stay locale-parameterised; no global theme/locale context is introduced (isolate-safe).
  • Existing a11y — aria-current/aria-disabled/aria-labelledby semantics are preserved; island primitives add WAI-ARIA behaviour on top.
  • Route paths, SSR/cache contracts, OG endpoints, wrangler config.

Positive:

  • One brand source feeds public site, dashboard, admin, docs, and OG images — drift becomes structurally impossible.
  • Public site goes unstyled → styled with zero added JS (Astro presentational primitives), keeping the Lighthouse gate green.
  • Owned, tested primitives satisfy the 100%-coverage bar; no vendor lock-in.
  • The future admin console (spec 10) reuses packages/ui wholesale.

Negative:

  • Two new packages (design-tokens, ui) and a Tailwind build step to maintain.
  • We write/maintain primitive components instead of npm install-ing them (offset by the coverage/lock-in wins, and bootstrappable from a template).
  • The Astro 6 rolldown-vite + Tailwind v4 compatibility issue must be resolved in the wiring PR.

Broken down in docs/superpowers/specs/2026-05-23-14-design-system-design.md (spec) and docs/superpowers/plans/2026-05-31-design-system.md (task-level plan), tracked as the epic + phase issues P0–P6 on GitHub. Each phase runs through workflows:tdd-cycle and must pass pnpm verify (lint + typecheck + 100% coverage + build) per spec 13.

Follow-ups (NOT in this ADR / out of MVP scope)

Section titled “Follow-ups (NOT in this ADR / out of MVP scope)”
  • Optional Style Dictionary + Figma token round-tripping once a designer is in the loop (the token package’s .css output stays the public contract either way).
  • Migrating docs-site/ brand overrides onto the token package is P1; full admin-console theming is P5 and depends on spec 10’s frontend landing.