Decisions

ADR-062: Design system + component strategy (Svelte vs Web Components)

The Platform UI (ai-frontend, SvelteKit + Svelte 5 runes) has grown a set of recurring visual primitives — KPI tiles, status pills, mode switchers, side rails, KpiCard, donut charts, timeline rows — that today live…

#062

ADR-062: Design system + component strategy (Svelte vs Web Components)

Status: Accepted Date: 2026-04-22 Related: ADR-058 (5-mode UI), ADR-059 (project-first IA), ADR-061 (extract pipelines + projects libraries)

Context

The Platform UI (ai-frontend, SvelteKit + Svelte 5 runes) has grown a set of recurring visual primitives — KPI tiles, status pills, mode switchers, side rails, KpiCard, donut charts, timeline rows — that today live inline or in src/lib/components/. They are not packaged, not versioned, not reusable outside ai-frontend.

The wider gyrum fleet has many web surfaces:

  • ai-frontend (SvelteKit, the Platform UI)
  • ai-research-dev (Go templ + plain HTML — the experiment runner UI)
  • buzzy-gyrum-io and other product front-ends (mixed stacks)
  • docs-viewer (plain HTML)
  • Future: import wizard pages embedded in product onboarding flows

Today every surface invents its own KPI tile, its own status colour scheme, its own typography choices. Drift is the default state. The operator's question on 2026-04-22 framed three issues at once:

  • Do we have design guidelines today? (Implicit, not formal.)
  • Are the components shared? (No — each surface re-invents.)
  • Do we have a layout guide? (Implicit, in +layout.svelte.)
  • Should we standardise on Svelte components or Web Components? (Open.)

This ADR resolves the four questions together because they are facets of the same decision.

Decision

Adopt a three-layer design system, packaged as a Web Components library with optional Svelte/React/Vue framework adapters, and formal design guidelines + layout guide documents.

Layer 1 — Design tokens

A gyrum-tokens package (CSS custom properties + JSON for non-CSS consumers) holding the single source of truth for:

  • Colour scales (mode palettes from ADR-058, semantic colours from app.css, status colours)
  • Spacing scale (4px base — t-0 t-1 t-2 t-3 t-4 → 0px 4px 8px 16px 24px)
  • Typography scale (font sizes, weights, line heights, font stack)
  • Radius scale, shadow scale, transition timings
  • Z-index scale (header, drawer, scrim, modal — explicit layers, no random z-index: 99999)

These are already partially established (src/lib/styles/mode-tokens.css, app.css); extracting them to a standalone package is a small refactor.

Layer 2 — Component library (Web Components)

A gyrum-ui package shipping framework-agnostic Web Components:

  • <gy-kpi-card value label tone>
  • <gy-status-pill status> (status: up | down | unknown | never-shipped)
  • <gy-mode-switcher modes selected>
  • <gy-side-rail> + <gy-side-rail-link>
  • <gy-data-table> (sortable, with status column convention)
  • <gy-fleet-donut> and <gy-bar-chart> (SVG, accessibility-first)
  • <gy-timeline> and <gy-timeline-entry>
  • <gy-empty-state> and <gy-error-state> (the calm/honest fallback patterns from the homepage redesign)

Each component:

  • Uses Layer 1 tokens via CSS custom properties — never hardcodes hex
  • Exposes a small attribute API + slots for content
  • Has a TypeScript .d.ts declaration so consumers get type safety
  • Has shadow DOM for style isolation (defaults can still be overridden via custom property pass-through)
  • Ships with a Storybook story
  • Has a Playwright/wdio cross-browser test (Chrome, Firefox, Safari)

Layer 3 — Framework adapters (optional, per-framework)

Web Components work in plain HTML, but framework users get a nicer DX from per-framework wrappers:

  • gyrum-ui-svelte — Svelte 5 component wrappers
  • gyrum-ui-react — React function wrappers (forwardRef, ts types)
  • gyrum-ui-vue — Vue 3 component wrappers (when needed)

Adapters are thin — they just re-export the Web Components with framework-idiomatic prop names and event names. The visual + behavioural contract lives in Layer 2.

Layer 4 — Design guidelines (documentation, not code)

A gyrum-ui-docs site (or a docs/ directory in gyrum-ui repo) with:

  • Principles: contrast, density, mode-tinting rules, "calm by default, loud only when actionable"
  • When to use what: KpiCard vs StatusPill vs DataTable cell — the decision tree
  • Layout guide: header anatomy, side-rail anatomy, content max-width, the responsive breakpoints (768/1080), the standard "hero / KPI / viz / list / timeline" page grammar from ADR-058's footprint
  • Mode-tinting rules: when chrome retints (content) vs stays neutral (header chrome, per ADR-058's PR #110 fix)
  • Empty / error / loading state recipes: never blank, always honest about what's pending or wrong
  • Accessibility rules: focus order, keyboard parity, ARIA conventions, contrast minimums

Why Web Components (not Svelte components as the package)

Five reasons, in priority order:

  1. Cross-stack reach. ai-research-dev's Go-rendered HTML can use Web Components today. Buzzy's product UI can use them. A future plain-HTML status page can use them. Svelte components only work in Svelte projects.

  2. Stable contract over time. Web Components are a browser standard (custom-elements, shadow DOM); their attribute API is durable. Svelte has had three major version migrations in three years (Svelte 3 → Svelte 4 → Svelte 5 runes); each broke component contracts. A standards-based contract survives the framework churn.

  3. Easier external adoption. If we ever open-source gyrum-ui (the way ADR-061 contemplates for gyrum-pipelines and gyrum-projects), Web Components are universally consumable. A Svelte-only library is a tiny addressable audience.

  4. Style isolation by default. Shadow DOM means a host page's CSS cannot accidentally bleed into a <gy-kpi-card>. The mode tokens pass through (CSS custom properties pierce shadow DOM), but the component's internal CSS is sealed. This is bigger than it sounds for a multi-product fleet.

  5. No build dependency at use site. A consumer can <script src="...gyrum-ui.js"> + use components in HTML. No webpack, no vite, no svelte-kit. The bar to using a gyrum-ui component is "include a script tag".

Why also ship Svelte adapters

Web Components have rough edges for Svelte authors specifically:

  • Two-way binding doesn't work the same way as native Svelte components
  • Slot APIs differ (named slots are more verbose)
  • Reactivity to attribute changes can lag (custom-element callbacks vs Svelte reactivity)

A thin Svelte 5 wrapper hides those rough edges for ai-frontend (the biggest Svelte consumer), while Web Components remain canonical.

Repository layout (gyrum-ui)

Same GitHub-standard skeleton as ADR-061's library extraction, with shape:

gyrum-labs/gyrum-ui/
├── README.md
├── LICENSE (MIT)
├── packages/
│   ├── tokens/              # CSS custom props + JSON
│   ├── components/          # Web Components (canonical)
│   ├── svelte/              # Svelte adapters
│   └── react/               # React adapters (when needed)
├── docs/
│   ├── principles.md
│   ├── tokens.md            # Visual reference of every token
│   ├── components/          # One md per component
│   ├── layout-guide.md      # Page grammar, breakpoints, anatomy
│   ├── tutorials/
│   │   ├── 01-first-component.md
│   │   ├── 02-mode-tinting.md
│   │   └── 03-svelte-adapter-walkthrough.md
│   └── faq.md
├── stories/                 # Storybook for visual testing
├── tests/
│   ├── unit/
│   ├── visual/              # Playwright + screenshot snapshots
│   └── a11y/                # axe-core
├── examples/
│   ├── plain-html/
│   ├── svelte/
│   ├── react/
│   └── go-templ/            # Yes, even Go templates can use Web Components
├── .github/workflows/
│   ├── ci.yml               # build + test + lint
│   ├── visual-regression.yml
│   ├── a11y.yml
│   └── release.yml
└── ...

Migration path

Like ADR-061's extraction, this is a sequence of small PRs:

Step Where What
1 gyrum-ui (new repo) Bootstrap, LICENSE, branch protection, skeleton
2 gyrum-ui Extract gyrum-tokens from ai-frontend's mode-tokens.css + app.css
3 gyrum-ui First Web Component: <gy-kpi-card> (the most-used today)
4 gyrum-ui Add Storybook + visual regression
5 gyrum-ui Svelte adapter for <gy-kpi-card>
6 gyrum-ui First v0.1.0 release (KpiCard + tokens + docs)
7 ai-frontend Replace inline KpiCard usage with gyrum-ui-svelte adapter
8 gyrum-ui Next components in priority order: StatusPill, ModeSwitcher, SideRail, DataTable, FleetDonut, Timeline
9 ai-research-dev First adoption of Web Components in Go-templ pages
10 gyrum-ui v1.0 once 6+ components have stabilised

Each step independently shippable. ai-frontend continues to build and ship throughout — no breakage required.

Testing strategy (TDD per ADR-061's discipline)

  • Unit tests per component (Vitest) — attribute reflection, slot rendering, event dispatch
  • Visual regression (Playwright + screenshot diff) — per component, per mode (5 themes), per viewport (mobile / tablet / desktop)
  • Accessibility (axe-core) — per component, hard fail on critical violations
  • Cross-browser — Playwright runs Chrome + Firefox + Safari (CI via Microsoft Playwright runners)
  • Adapter parity — Svelte adapter outputs the same DOM as direct Web Component usage; React likewise. Fixture-driven, like ADR-061's cross-language parity for pipelines/projects.

Consequences

Easier:

  • Cross-product visual consistency without coordinated rewrites
  • Mode-tinting becomes a token problem, not a per-component problem
  • New product surfaces (status pages, marketing sites, embedded widgets) get the gyrum look for free
  • The Web Component standard outlives any Svelte/React/Vue churn
  • Open-source path: gyrum-ui can go public the same way gyrum-pipelines and gyrum-projects can

Harder:

  • Three packages (tokens, components, adapters) instead of one
  • Visual regression infrastructure to maintain (screenshot baselines drift; rebaselining is friction)
  • Shadow DOM has known interop quirks (form elements, focus management) that need component-level fixes per browser
  • Svelte adapter is a permanent commitment to mirror Web Component changes

What we sign up to maintain:

  • The token catalogue (docs/tokens.md) as the single source of design truth — no PR adds a one-off colour without registering it as a token
  • Visual baselines in version control — every component change updates baselines, baselines reviewed in PR
  • The principle "modal/scrim/header z-index has explicit values, no surprise stacking" — enforced via lint on z-index: literals in ai-frontend going forward

Alternatives considered

  • Stay Svelte-only with a shared gyrum-svelte-ui package. Rejected: ai-research-dev (Go templ) and external products (mixed stacks) cannot consume it. Locks gyrum to Svelte forever.
  • React-based shared library. Rejected: same problem in reverse. Lit/Stencil/Web Components is the only stack-neutral option.
  • CSS framework (Tailwind, Pico, Bulma). Rejected: solves styling but not behaviour; we still rewrite the KpiCard in every framework. Tokens are useful, but full components beat them for consistency.
  • No shared library, just a style guide doc. Rejected: docs without code drift. The KpiCard exists in three near-identical implementations today; another six months and we'll have eight.
  • Use an off-the-shelf Web Components library (Shoelace, Adobe Spectrum, Carbon). Rejected: those carry opinionated visual language we don't own and can't fully tune. The mode-tinting rule from ADR-058 is hard to retrofit onto third-party components.

Related ADRs

  • ADR-058: 5-mode UI — defines the mode-tinting rules gyrum-ui consumes from gyrum-tokens
  • ADR-059: project-first IA — components like <gy-status-pill> and <gy-data-table> are how project pages render their data
  • ADR-061: library extraction precedent — same skeleton, same TDD discipline, same GitHub-standard checklist
  • ADR-060: catalog-driven infrastructure — catalog viewers are rendered with gyrum-ui components

Open questions for follow-up

  • Hosting Storybook publicly (under what URL, how often deployed)
  • Visual regression tolerances — pixel-perfect or perceptual? (Lean perceptual; pixel-perfect is too noisy across browser versions)
  • npm publishing (scoped @gyrum-labs/... matching ADR-061's choice)
  • Should the design tokens also ship as Tailwind config / Figma tokens for designers? Probably yes; out of v1 scope