rgoussu@goussu: ~/library/frontend/exercises
~/library/frontend/exercises cat design-system-from-scratch-subject.md

Design system from scratch — subject

# The full work statement for the design-system build — component inventory, three-tier tokens with a light/dark swap, per-component a11y contracts, Storybook, versioned package.

Subjectsaved 2026-08-08 #exercise#frontend#design-systems#atomic-design#accessibility#typescript#subject

Brief

You are the first design-system engineer at a small product company. Two product teams are shipping the same UI twice, with diverging colors, inconsistent focus states, and accessibility bugs fixed in one app but not the other. Your assignment: build the shared system — tokens, components, docs, package — that both teams adopt, and prove it holds by re-theming a consuming app without touching a single component.

Instructions

1. Token layer (three tiers)

Build the token architecture as three explicit tiers, each defined only in terms of the tier below:

  • Primitive — raw values with no meaning attached: the palette (blue-500: #2563eb…), a spacing ramp (space-100space-800), a modular type scale, radii, shadows.
  • Semantic — intent, mapped onto primitives: color.action.primary, color.surface.default, color.text.muted, color.border.error, color.focus-ring, space.gap.field, font.body, font.heading.
  • Component — per-component slots mapped onto semantics, only where a component genuinely needs its own knob: button.primary.bg, input.border, modal.backdrop.

Emit the tokens as CSS custom properties from a single source of truth (JSON/TS module). Implement the dark theme by redefining only the semantic tier — primitives and component tokens do not change. Ship a token showcase page rendering every token in both themes.

2. Component inventory

Build exactly this inventory, in atomic-design order. Every component: semantic HTML underneath, styled only through tokens (no hard-coded values), typed props with variants as discriminated/literal unions (no boolean soup), visible focus state via color.focus-ring.

Atoms

  • Button — variants primary | secondary | ghost | destructive; sizes; disabled and loading states; renders a real <button>.
  • Input — text input with invalid state; never communicates error by color alone.
  • Label — always associated to its control (htmlFor).
  • Icon — decorative by default (aria-hidden), accessible name required otherwise.

Molecules

  • Field — Label + Input + hint + error message; error wired via aria-describedby, invalid state via aria-invalid; one Field used for every form row.
  • Card — heading, body, actions slot; correct heading-level handling.

Organisms

  • Modal (dialog) — follows the APG dialog pattern: focus moves in on open, is trapped while open, Escape closes, focus restores to the trigger on close; role="dialog" + aria-modal + labelled by its title.
  • Nav header<header>/<nav> landmarks, current page marked with aria-current="page", fully keyboard-traversable.

3. Accessibility contract (per component)

For each component, before writing code, write down its contract in three columns — keyboard (every key the APG pattern demands), focus (where focus goes and when), ARIA (roles/states/properties) — from the relevant WAI-ARIA APG pattern. Implement the contract at the component level so consumers get it for free. A keyboard-only walkthrough of a demo form + modal + nav must succeed with no pointer.

4. Storybook

One story per component state (not just per component), do/don't usage docs on each component page, the a11y addon enabled, and axe checks running in CI so a violation fails the build.

5. Package & prove

Version the library (semver), publish it as an npm workspace package or tarball, and consume it from a separate small demo app. Then execute the proof: switch the demo app to the dark theme by swapping the semantic token definitions only — zero component or app-markup changes.

Constraints

  • No component library underneath (no MUI/Chakra); headless primitives are reserved for the stretch goal comparison.
  • No hard-coded color/spacing/font values inside components — tokens only. Grep for hex codes in component source; finding one is a defect.
  • No div-with-onClick interactive elements; native semantics first, ARIA only to fill genuine gaps.

Acceptance

Mapped one-to-one onto the exercise's milestones:

  1. Token layer — the showcase page renders every token; the dark theme diff touches only semantic-tier definitions; component and primitive files are byte-identical across themes.
  2. Atoms — Button, Input, Label, Icon render in both themes with visible focus rings; props compile as unions (an invalid variant is a type error, not a runtime fallback).
  3. Molecules & organisms — Field announces its error to a screen reader via aria-describedby; the modal passes the APG dialog checklist (trap, Escape, restore); the whole demo is operable keyboard-only.
  4. Storybook — every component state has a story; axe runs in CI and the build fails on a seeded violation (verify by seeding one).
  5. Package & prove — the demo app consumes the published package by version; the theme swap commit contains no changes under the component source tree.

Related