At a glance
I bet that if AI agents generate every component, a design system needs no component library. It needs rules precise enough for a machine to follow and check.
- 117
accessibility failures blocked before merge, since July 2026
- 2.5 days
median time to first gate pass across 9 pilots, down from 9 days for the first to under 1 for the latest
- 71%
first-run gate pass rate, n = 186 pilot builds
-
3
North star
products in production on a pinned release, including the AI agent and home hub, the entry point to every product
The first design system reached 6 products in over six years. The spec inherited its asset pipeline, and the AI coding agents the spec depends on arrived late in those six years.
The problem
My first design system was a component library I built. It had 45 components, with React, Vue and Angular built simultaneously. I delivered it in four stages against a roadmap I presented at CTO level.
For three years I led a multidisciplinary team on our first design system. When both team members moved on, I was running it alone, and a library that needed every component built three times stopped being feasible for one person.
Running the static gate against existing products found hundreds of rule violations per product. Over six years, 6 products adopted it, to varying degrees, across three frameworks. Each of its components cost about 3.5 weeks across three framework builds, roughly two of them on the second and third builds. And nothing told an AI coding agent what on-brand or accessible meant.
Before
- Designer draws the component
- Handoff to engineering 1 to 2 week wait
- Central team builds and releases 4 to 6 week wait
- Product team adapts it
- Accessibility audit
After
- Team pins a release of the spec
- Their AI agent reads the contract
- Agent generates the UI
- The gate checks it in CI
- People review what machines can't judge
Drift: static gate on the 12 pilot products, mostly raw colour values, then off-scale spacing. Durations: delivery history of the first design system.
Options
I judged three options on five criteria: framework coverage, time to ship, drift control, maintenance cost and dependence on a central team.
Option A
Keep the first design system
Keep the library and its three framework builds.
Why it lost. With both team members gone, one person would have built every component three times, for low adoption.
Option B
Generate code from Figma
Generate components from Figma through a framework-agnostic JSON contract I piloted.
Why it lost. It generates from a picture, so the rules never reach the code.
Option C, chosen
Specify and verify
A specification that teams' agents build from, and a gate that checks the result.
What the pivot cost
- 45 components of work on the first design system, set aside
- Teams who had only just migrated to it
- Its roadmap, which I had committed to at CTO level
How I handled it. The asset pipeline carried over intact and migrated teams got a token mapping from the first system. I re-presented the CTO roadmap with new milestones.
What I built
The spec covers 21 components and 6 layout primitives, with more than 90 releases since July 2026.
One spec, any framework
Tokens, component rules and the definition of done live in three plain-text files.
Accessibility lives in the tokens
Every colour pairing the spec allows is tested against WCAG 2.2 AA in both themes, and a failing pair blocks the release.
A release is a contract
Teams pin a tagged release, and a hash records exactly which spec they pinned. The gate checks every build against it.
Read a spec excerpt on focus rings
Focus ring rendering. Every focus ring, the outline drawn on
:focus-visible, declares the spec's focus ring offset token, whether or not the element has a fill. Leaving the offset undeclared, which hands the gap to the browser's default, is off-contract. So is 0, a negative offset or any other value, even on an element where a flush ring would clear 3:1.A flush ring fails WCAG 1.4.11 on filled variants. Measured against each fill, it reaches 1.31:1 on the default interactive fill and 1.51:1 on the accent fill. The offset places the ring on the canvas instead, where it measures 4.39 to 13.04:1 across all four surfaces a focused control may sit on. So the offset rule is what carries 1.4.11, not the colour choice.
Each visual choice is a named rule with a token behind it. Visual rules an AI agent can follow shows how.
The gate
I built a gate with Claude Code that blocks any change that breaks the contract.
Static
Reads the source on every commit. It flags raw colours, off-scale spacing, unknown tokens, broken type pairings and right-to-left breakage.
Rendered
Opens a real browser before merge. It measures contrast on real pixels in both themes and tabs through every control.
Human
Says where automation stops. A screen can pass the gate and still be the wrong screen, so reviewers spend their time there.
When the accessibility engine could not run on a page, the gate used to report a pass, so now a file the gate can't evaluate fails the run as incomplete. Read the full decision log entry in Running a design system with 15 agents.
Try the gate
Pilot progress
passes: static, rendered checked 2 governed file(s) PASS, 0 violations
See a real gate run
$ node src/cli.js --config fixtures/violating/gate.config.json gate — validated against pinned spec v0.31.0 (manifest sha256 verified) passes: static checked 7 governed file(s) bad.css bad.css:2 [colour] raw hex colour `#ff0000` — use a semantic `--color-*` token bad.css:4 [spacing] off-scale spacing `15px` — not a scale step; nearest is `16px` (use its `--spacing-*` / `--space-*` token) bad.css:12 [undefined-token] `--color-nonexistent` is not a defined colour role token in the pinned spec bad.css:41 [tokenisable] `linear` is for loops only — a transition takes one of the four cubic-bezier easings, because a constant-velocity transition reads mechanical bad.html bad.html:5 [a11y] <img> is missing required "alt" attribute (wcag/h37) laundered.css laundered.css:17 [spacing] hardcoded length `13px` in `--card-pad`, which is not a spec token — the value reaches governed properties through `var(--card-pad)` unchecked; use a spec token, or define this property from one laundered.css:41 [ancestor-custom-property] `--ring-glow` holds a var() and is declared on `:root`, so the var() resolves on the ancestor and descendants in another theme inherit the wrong value. Declare `--ring-glow` on the component selector whose own declarations read it. DESIGN.md § Focus ring rendering. unenforced-prose-rules.css unenforced-prose-rules.css:58 [logical-properties] physical side `padding-left` — write `padding-inline-start` (`paddingInlineStart` in a style object) with the same value, so the layout mirrors in right-to-left locales. COMPONENT-STANDARD.md § 5 mandates logical properties. [… 50 further findings omitted …] Exceptions applied (1) — auditable, deliberate opt-outs: exception-demo.css:3 [color] reason: third-party chart lib requires this exact brand hex FAIL — 58 violation(s): 13 colour, 16 spacing, 15 tokenisable, 3 undefined-token, 6 a11y, 1 bad-exception, 1 ancestor-custom-property, 2 logical-properties, 1 typography EXIT=1
First evidence
I had only checked the spec by reading it, so I tested it by building from it.
Build
Three fresh AI sessions got only the pinned spec and built the same components.
Measure
All three failed the rendered gate on one button state no static check could see.
Fix
I corrected the rule, and every release that changes a rule now gets this test.
Caught by the gate
A rule that broke its own standard
The button rule gave its label 3.34:1 against 4.5:1 on its active surface, and the same paragraph cited that ratio to ban the pairing elsewhere.
Missed by the gate
A rule that contradicted itself
A table rule asked for two things that can't both be true. The agent picked one, wrote clean code and noted the conflict, so the release process now reads the agent's notes too.
n = 3 builds with one agent. A Cursor rerun passed two of three first time, and the gate caught an invented token in the third. This supports the method, but the model itself still needs adoption data.
Adoption
Engineering leadership made the spec the default for new UI and reskins, with no mandate to rewrite existing screens.
Why redo work we've only just finished?
What changed their minds
Speed
In pilots, a screen took a median of 1.5 days vs 5 through the old handoff, across 5 screens built both ways. Most of the saving was handoff wait removed.
No cost to them
I built the first three implementations. Later teams self-serve with a one-command setup.
The outcome was “We love it, but we can't do it right now.” Rollout follows each team's roadmap. A funded reskin was the forcing function, and mandates and demos converted nobody.
Of about 60 teams, counted for the weekly report.
Designers now build in the same session as the agent. So far, 7 designers have contributed 47 catalogue entries, and Running a design system with 15 agents shows how.
Where I changed my mind
I argued rollout should wait until the component gap list closed. Then a smoke test found six contract defects review had missed, so pilots started with the first component batch and became our main way to find them.
We stopped arguing about pixel values in review. The gate settles that, so review is about whether the screen works.
Measures and trade-offs
North star. Products in production on a pinned release, counted weekly.
Guardrail. I watch how long a critical fix takes to reach every adopter. Tagging one takes under 1 working day, and reaching all three products took 6 working days.
Next: reach 1.0. Four of six exit criteria are met; still open are a first-run pass rate of 75% or more over 100 builds and one quarter with no token contract change.
Trade-offs
The decision log records what each major choice costs.
AI output varies from run to run
Cost
Three runs from one brief used the same tokens and values but structured the code differently, from 407 to 930 lines.
Contained by governing the contract, not the code
The gate checks tokens, contrast and behaviour, which held steady. Product teams own generated code through their own reviews and tests.
Existing products weren't built for this
Cost
Most early adopters have a shipping product, its own CSS and a designer with expectations.
Contained by writing the guide for existing products first
It names the four problems they hit, in order. Server-rendered legacy products take about 2 to 3 weeks to a first screen.
The gate can't prove complex widgets behave correctly
Cost
The gate checks tokens, contrast, focus and ARIA, but not whether a complex widget behaves as users expect.
Contained by splitting the checks between the gate and people
Six of the contracts are complex widgets. The gate checks all six for tokens, type scale and static ARIA, and its rendered checks cover names, roles and text contrast. For the modal it also tests the focus trap, Escape and focus return. People check the rest, such as announcement order, typeahead, arrow-key focus and dismissal.
Pinning slows critical fixes
Cost
Pinning protects teams from unplanned changes, so a critical fix waits for each team to update its pin. That wait grows with every adopter.
Contained by updating pins through automated pull requests
Every tagged fix opens one for each adopter. Teams still review and merge it, so the pin keeps protecting them.
Reflection
Remove the cost of saying yes
Teams said yes once the implementation work wasn't theirs.
Retire your own work when the evidence says so
Sunk work is a poor reason to keep a product alive.