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
Context
Section titled “Context”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.
Decision
Section titled “Decision”Adopt a three-layer design system:
-
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. -
Tailwind v4 via
@tailwindcss/postcss(CSS-first@themereading the token variables; the deprecated@astrojs/tailwindintegration is not used). Zero-runtime, purged utilities → Lighthouse-friendly. Transport note: the@tailwindcss/viteplugin 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.mjsis the chosen path. The CSS-first@thememodel is identical either way. Tokenvar()mappings use@theme inlineso dark-mode token swaps are not shadowed. -
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 thespotlight*/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-solidwas 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.
Alternatives considered
Section titled “Alternatives considered”| Option | Why rejected |
|---|---|
| Plain Tailwind only | Solves utilities, not accessible behaviour (Dialog/Combobox/Calendar focus + keyboard). Necessary but insufficient — kept as Layer 2, not the whole answer. |
| daisyUI | Zero-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 / Mantine | Large 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 object | Doesn’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. |
What stays unchanged
Section titled “What stays unchanged”- 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-labelledbysemantics are preserved; island primitives add WAI-ARIA behaviour on top. - Route paths, SSR/cache contracts, OG endpoints, wrangler config.
Consequences
Section titled “Consequences”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/uiwholesale.
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.
Implementation
Section titled “Implementation”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
.cssoutput 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.