Skip to main content
Nicolas Ménard
ProjectsAbout meExperiments
ResumeOpen ↗

Design System

Keeping Figma and React in Sync with DESIGN.md

A token-driven design system built in parallel across Figma and React, where a single markdown contract lets AI agents do the execution without drift.

Role
Solo designer-engineer
Scope
Design system · Figma ↔ React parity · AI-agent workflow
Outcome
Working spec app with automated token-drift and accessibility checks
Status
Completed
Stack
Figma · Figma MCP · Claude Code · Codex · React 19 · Vite · TypeScript
Published
April 2026
Links
GitHubFigmaLive Demo

01 / Source

Figma + MCP

Queryable variables,
components, and variants.

02 / Execute

AI Agents

Inspect, patch, and run
the checks that matter.

03 / Output

Tokens + UI

primitives.json,
semantic.json, React components.

04 / Verify

Spec App + Tests

Catches drift before shipping,
enforced, not narrated.

Most design systems die at the Figma → code handoff. The file drifts, the tokens fork, the components get rebuilt twice, and within six months nobody can answer a simple question: where is the truth?

Factory is an experiment in making that handoff automatic. It is a token-driven spec app built around one canonical visual language – Geist Mono, orange and ink, square edges – where every primitive, semantic alias, and component contract lives in machine-readable files that both humans and AI agents can execute against.

What's in it

  • 12 routes (1 overview, 4 foundations, 7 components)
  • 14 component families – Button, InputField, Select, Textarea, Checkbox, Radio, Toggle, Link, Badge, Alert, Card, Tab, and two form primitives
  • 53 primitive tokens and 92 semantic aliases
  • 33 curated Remix icons (no open-ended icon library)
  • 7 test suites covering token drift, component behavior, docs acceptance, and jest-axe accessibility smoke tests
  • One DESIGN.md file governing the whole thing

Four pillars shape every decision: Source (Figma variables and components), Method (primitive → semantic → component, enforced in both directions), Identity (orange, ink, square edges, Geist Mono, no exceptions), and Output (generated tokens and inspectable specs, not screenshots).

The stack is intentionally small. Figma and Figma MCP handle design input. Claude Code and Codex handle execution – reading the contract, diffing Figma against code, writing patches, and running the test suite. React 19 + Vite + TypeScript is the runtime. Every other choice flows from one rule: the markdown contract is the source of truth, and everything else is its expression. This article is a case study of what that looks like in practice, what it took to build, and what I'd do differently next time.

token-brand-system
Explorer
TOKEN-BRAND-SYSTEM
.claude
.github
dist
docs
node_modules
public
scripts
src
components+
docs
generated
styles
test
tokens+
{ }primitives.json
{ }semantic.json
.gitignore
App.cssM
App.tsxM
main.ts
site-env.d.ts
AGENTS.mdU
CONTRIBUTING.md
DESIGN.md
index.html
LICENSE
{ }package-lock.json
{ }package.json
OUTLINE
TIMELINE
LOCAL HISTORY
DESIGN.md

Factory Design System

Summary

Factory Design System is a token-driven spec app built around one canonical visual language:

  • Typeface: Geist Mono
  • Brand palette: orange + ink
  • Semantic blue: informational-only

The repo is intentionally structured so the token inputs are the single source of truth and the spec site is the visible expression of those tokens.

Collaboration Source Of Truth

  • The repo is canonical for accepted design-system changes.
  • Figma may diverge only in WIP exploration areas.
  • Published Figma variables and components must match main.
  • Canonical Figma changes require a matching repo PR.

Source of Truth

Only these files define token values and aliases:

  • src/tokens/primitives.json
  • src/tokens/semantic.json

All generated CSS variables and token docs derive from those files.

Artifact Governance

The repo has more than one canonical layer. Each artifact has a different job:

  • Token values and alias relationships:
    • src/tokens/primitives.json
    • src/tokens/semantic.json
  • Global design language, layout policy, accessibility policy, and route contract:
    • DESIGN.md
  • Normalized component behavior contracts:
    • docs/component-specs.md
  • Figma execution checklist for system edits:
    • docs/figma-edit-checklist.md
  • Icon subset, growth rules, and semantic usage:
    • docs/icon-guidelines.md
  • Rendered specimens, examples, and route copy:
    • src/docs/routes.tsx
  • Runtime implementation details:
    • src/components/**
  • Consistency enforcement:
    • src/test/**

Do not treat the routed spec pages as the only source of truth for behavior. They are the rendered expression of the authored contracts above.

Token Pipeline

primitives.json and semantic.json are processed by scripts/generate-tokens.mjs.

That script is responsible for:

  • validating token references
  • generating src/generated/primitives.css
  • generating src/generated/semantic.css
  • generating src/generated/token-docs.ts

The build and test scripts always run token generation first.

Token Taxonomy

Primitives

Raw values only. No meaning beyond the value itself.

  • color
    • orange, ink, sand, red, green, blue
  • space
  • radius
  • borderWidth
  • elevation
  • iconSize
  • fontFamily
  • fontSize
  • lineHeight
  • fontWeight
  • opacity

Generated CSS format:

  • --primitive-color-orange-9
  • --primitive-space-4
  • --primitive-border-width-strong

Semantics

Meaningful aliases that components consume.

  • color.action
  • color.surface
  • color.text
  • color.border
  • color.feedback
  • focus
  • layout
  • radius
  • borderWidth
  • elevation
  • iconSize
  • opacity
  • typography

Generated CSS format:

  • --color-action-primary
  • --color-feedback-info-surface
  • --focus-outline-color
  • --layout-page-inset
  • --typography-heading-size

Route Structure

The app uses HashRouter and mirrors the design-system information architecture.

Overview

  • #/overview

Foundations

  • #/foundations/colors
  • #/foundations/typography
  • #/foundations/layout
  • #/foundations/icons

Components

  • #/components/buttons
  • #/components/forms
  • #/components/links
  • #/components/tabs
  • #/components/badges
  • #/components/alerts
  • #/components/cards

Route Contract

The Overview route is intentionally different from the rest of the system. It is the narrative entry point and must include:

  • system model
  • reference implementation
  • accessibility notes

Every foundation and component route must include:

  • state coverage
  • usage guidance
  • token mapping
  • accessibility notes

Optional sections such as best practices, curated galleries, or interactive demos may be added when they clarify the contract.

Component Contracts

Button

  • Variants: primary | secondary
  • Sizes: sm | md | lg
  • Props:
    • leadingIcon?
    • trailingIcon?
    • loading?

Required documentation states:

  • default
  • hover
  • focus
  • pressed
  • disabled
  • loading

Forms

Shared field contract across InputField, Select, and Textarea:

  • label
  • helperText
  • error
  • disabled
  • readOnly
  • required

Required documentation coverage:

  • default
  • hover
  • focus
  • error
  • disabled
  • read-only
  • helper/error text
  • select/date guidance

Date input remains spec-only in this phase.

Link

  • Appearances: default | inverse
  • States:
    • default
    • hover
    • focus
    • visited
    • disabled

Tabs

  • Props:
    • value
    • onValueChange
    • items: { value, label, icon?, count?, disabled?, content? }[]

Behavior requirements:

  • explicit tab and tabpanel ids
  • arrow key navigation
  • Home/End navigation
  • icon and count variants
  • overflow/mobile guidance

Badge

  • Tones:
    • brand
    • info
  • max controls overflow formatting

Badge semantics rule:

  • brand is the filled New state
  • info is the outlined Informational state and must not use action tokens as its fill

Alert

  • Tones:
    • neutral
    • info
    • success
    • attention
    • error
  • Props:
    • action?
    • dismissible?
    • showIcon?

Accessibility rule:

  • use role="status" for non-disruptive tones
  • use role="alert" for disruptive attention/error tones

Semantic rule:

  • the former white "info" treatment is now neutral
  • blue info is a true informational tone

Card

Documented behaviors:

  • static vs clickable
  • hover/focus lift
  • square vs landscape media ratios
  • CTA hierarchy

Visual Rules

  • Orange is the only brand accent.
  • Ink surfaces and text anchor the system.
  • Blue is reserved for informational semantics.
  • Do not reintroduce a second documentation font.
  • Do not use primitive color tokens directly in component CSS.

Accessibility Rules

  • Focus must remain clearly visible on all interactive elements.
  • Touch targets must stay at or above 44px.
  • Labels must remain visible for form fields.
  • Tabs must preserve correct ARIA wiring and keyboard interaction.
  • Alerts must use appropriate live-region semantics.
  • Links must remain identifiable before hover.

Page Annotation Rules

Any page, screen, or flow documented through this system should annotate page-level structure as well as component-level behavior.

  • Heading hierarchy:
    • one page-level heading
    • section headings must descend in order
  • Landmarks:
    • annotate primary regions such as header, navigation, main, complementary, and footer when they exist
  • Semantic order:
    • document reading order and source order when the visual layout could imply a different sequence
  • Focus order:
    • note where focus enters, how it moves through composite widgets, and where it lands after dismiss or navigation actions
  • Status and live messaging:
    • identify when passive status uses status semantics and when disruptive messaging uses alert

Page annotation rules are part of the authored system contract even when the routed spec page is primarily visual.

Testing Rules

The repo must keep these checks passing:

  • npm run build
  • npm run test

The automated suite covers:

  • token reference validation and generated-file drift
  • component behavior for buttons, alerts, badges, tabs, and fields
  • docs-route acceptance coverage
  • accessibility smoke tests with jest-axe

Implementation Guardrails

  • Update token JSON first, then regenerate outputs.
  • Keep generated files deterministic and committed.
  • Prefer semantic tokens in components.
  • Use the routed docs app as the primary artifact, not a single long gallery page.
  • When a new component or state is added, update both the runtime component and its route-level spec coverage.

The problem: drift is structural, not accidental

Figma and code drift because they each store the same truth in two different languages. Designers edit pixels; developers edit values; neither side sees the other's edit until a handoff meeting, and by then the drift is already shipped. Every design system I've worked on has had this same rot at the seams.

Tokens are usually a patch, not a spine. The standard fix is to ship a tokens.json late in the project and hope the Figma variables mirror it. They rarely do. Figma tokens get renamed to match Figma's ergonomics, code tokens get renamed to match build-tool ergonomics, and the JSON becomes a third file that neither side owns. You end up with three sources of truth and no contract.

So what would a real contract look like? I wanted to find out whether a markdown file – human-readable but machine-parseable, versioned in git, and explicitly opinionated – could be the canonical spec that both Figma and production code obey. Google's Stitch describes a methodology called design.md in this direction. Jocelyn Lin's DLS-2026 case study explores a parallel idea, using AI to generate tokens from a brand brief.

I wanted to push further: not just use a markdown spec to generate a system, but treat it as the contract AI agents execute against every time the system changes. Factory is the test bed for that idea.

01 / Primitives

Raw values only

Concrete, reusable values. No product meaning yet.

color-orange-9

#E8520A

space-4

16px

radius-none

0px

02 / Semantics

Intent over value

Roles map raw tokens into intent. Components consume this layer.

color-action-primary

→ color-orange-9

layout-section-gap

→ space-8

radius-control

→ radius-none

03 / Components

Semantic-only UI

Components consume semantic tokens only. Primitives never enter UI code directly.

background

var(--color-action-primary)

padding

var(--space-control-md)

border-radius

var(--radius-control)

⚠Components reference semantic tokens only, never primitives directly.

The design process: constraints first, code second

Three moves, in order.

1. Write DESIGN.md before writing any code

The first file I committed to the repo was a markdown file that described every rule the system would enforce – visual language, accessibility policy, artifact governance, token taxonomy, route contract, testing guardrails. No components, no tokens, no CSS. Just rules. Roughly 320 lines, opinionated, quotable.

That file became the brief I hand to every AI agent that touches the system. When Claude Code starts a task, the first thing it reads is DESIGN.md. When Codex generates a component stub, it checks DESIGN.md's visual rules. When a new contributor lands in the repo, they read DESIGN.md before touching a line of code. The contract is the brief is the spec.

Garry Tan's April 11, 2026 article, "Thin Harness, Fat Skills", gave me a useful name for the pattern I was already converging on: keep the harness small, and move reusable judgment into explicit markdown contracts and runbooks. In Factory, that means the harness reads the repo, loads the right files, runs narrow tools, and verifies deterministic outputs, while DESIGN.md, the Figma checklist, and small task-specific docs hold the process that should persist across tasks.

2. Force brutal constraints to shrink the surface

AI agents reason better against opinionated rules than polite suggestions. So I wrote the rules as manifestos:

  • Geist Mono is the system voice across UI, labels, and headings.
  • Orange is the only brand accent.
  • Blue is reserved for informational semantics only.
  • Radius: one value. No exceptions.

Infrastructure with guardrails, not documentation. Docs describe; infrastructure enforces. If a rule can't fail a build, it isn't a rule.

Radius is the proof. There is exactly one radius token in the system (0px), pinned on the Radius foundation page as an easter egg, and any component that introduces a second fails the test suite.

Constraint isn't austerity. It's compression. With these rules in place, the agent's decision space collapses from "which of 47 spacing values?" to "which of 9, and why not one of the other 8?"

3. Separate the token layers explicitly

Three files, three jobs:

  • primitives.json – raw values only, no meaning (e.g. --primitive-color-orange-9)
  • semantic.json – role aliases that components consume (e.g. --color-action-primary)
  • Component CSS – consumes semantic tokens only; primitives are banned at the component layer

A build script (generate-tokens.mjs) validates every reference, flattens the aliases, and emits generated CSS and a typed docs module. The generated files are committed, so drift is catchable in git diff. The test suite fails the build if any generated file is stale.

The layers are the spine. Everything else bolts onto them.

The solution: design ↔ code parity with AI agents

This is the section that earns the framing. Three execution moves.

1. Figma ↔ code parity via Figma MCP

Claude Code has a Figma MCP tool that exposes get_design_context, get_metadata, and get_screenshot against any Figma node. My workflow for any visual change became:

  1. Point the agent at a Figma node
  2. Ask it to diff the node against the coded component
  3. Patch only what's out of sync

Concrete example. Midway through the build I updated the Button's Loading state in Figma to use a Remix loader-4-line icon instead of a CSS border spinner. I told Claude Code to reconcile it. The agent pulled the variant metadata (183:182: Size=Large, Variant=Primary, State=Loading), saw that the Figma version referenced a remix-icons/line/system/loader-4-line icon I didn't have in my curated subset, read the SVG from my node_modules/remixicon folder, added the path data to the Icon.tsx curated dictionary, and swapped the Button's spinner div for the new <Icon> component. Type-check passed, build passed, tests passed. The turnaround was seven minutes.

The important thing isn't the speed. It's that the agent never invented anything – every move was bound to an explicit rule from DESIGN.md or the token files. When it ran out of information, it stopped and asked.

claude-code · reconcile Button Loading state
7 min · solo
  1. 00:00→

    Pull variant metadata from Figma

    183:182 · Size=Large · Variant=Primary · State=Loading

  2. 00:43→

    Detect missing icon in curated dictionary

    remix-icons/line/system/loader-4-line

  3. 02:10→

    Read SVG from node_modules/remixicon

    add path data to Icon.tsx curated dictionary

  4. 04:32→

    Swap spinner <div> for <Icon /> in Button.tsx

    reference semantic --color-action-primary (not primitive)

  5. 06:55✓

    Run typecheck · build · test suites

    all green

Bound only to rules in DESIGN.md + token files● typecheck · build · tests green
Agent session: Button Loading state reconciled from Figma to code. The agent never invented anything. Every move is bound to an explicit rule.

2. Every spec page tells the same story

Foundations and components in the docs site are composed from six layout primitives: SpecPageLayout, StateMatrix, TokenMappingTable, UsageGuidance, AccessibilityNotes, and DoDontExamples. Every page uses the same sections in the same order.

This repetition is pedagogy. A reader who spends thirty seconds on the Button page knows how to read every other page in the system. More importantly, the agent knows it too: adding a new component is a fill-in-the-blanks job against a known template, not a bespoke design pass. The structure of the docs is itself a machine-readable contract.

01· Page frame
SpecPageLayout

Hero, anchored table of contents, section order. Every page renders the same scaffold.

02· State coverage
StateMatrix

default · hover · active · focus · disabled for every variant. Missing cells fail the build.

03· Token resolution
TokenMappingTable

Semantic → primitive chain for every token the component consumes. Generated, not written.

04· When to use
UsageGuidance

Narrow rules for when to reach for the component, and the named alternatives when not.

05· A11y contract
AccessibilityNotes

Roles, keyboard order, contrast minima, motion expectations. Enforced in jest-axe tests.

06· Boundaries
DoDontExamples

Correct and incorrect compositions rendered side-by-side. Mistakes shown, not described.

The spec template. Every foundation and component page composes from these six primitives, in this order.
nicolasmenard.design/experiments/factory#/overview
View Full Page ↗
Live embed of the Factory spec app. Open full page for the complete navigation.

3. The Figma side has its own contract

docs/figma-edit-checklist.md is the inverse of DESIGN.md. When the Figma file needs to change – a new variant, a new state, a token tweak – the checklist forces the editor to declare:

  • What primitive and semantic tokens map to the change
  • What component contract the change implements
  • What the responsive failure mode is
  • What the accessibility intent is

The checklist isn't a suggestion. It's the done-criteria for a Figma edit. An edit that can't answer those four questions isn't complete, regardless of how good the pixels look.

Together, DESIGN.md + figma-edit-checklist.md + the token JSON files form a triangle. Each side references the other two. An AI agent working on either Figma or code has a stable reference point on every decision.

Contract

DESIGN.md

Visual rules, governance, route contract, test guardrails.

Design input

figma-edit-checklist.md

Declares tokens, component contract, responsive and a11y intent for every Figma edit.

Token inputs must match both.

Token source

primitives.json · semantic.json

Raw values and role aliases. Generated CSS and docs derive from these.

Each artifact references the other two. Every AI agent has a stable reference point on any decision.

Implementation & results

Numbers

Before (manual)With AI agents
New component timehours to days~20 minutes
Figma ↔ code driftsilent and permanentsurfaced by the checklist
Token authoringad-hoc values in CSSprimitives.json → generated CSS
Accessibility coveragedeferred to auditenforced in the test suite
Docs writing timehoursminutes (template)

Guardrails that caught regressions

Seven test files enforce the contract:

  • tokens.test.ts – token reference validation and generated-file drift
  • button.test.tsx, alert.test.tsx, badge.test.tsx, tabs.test.tsx, fields.test.tsx – component behavior
  • docs-routes.test.tsx – every route must render state coverage and accessibility guidance

The build runs token generation first and fails on any drift. npm run build is the contract-enforcement command, not npm run test. Drift is a build failure, not a test failure, because drift is a build-time fact.

What shipped

A 12-route spec app. 14 component families. 53 primitive tokens. 92 semantic aliases. 33 curated icons. Zero primitive tokens leaking into component CSS. One markdown contract governing it all. A few days, solo, with Claude Code and Codex as the execution layer.

What still doesn't work

Three honest caveats, because every case study needs them:

  • Variant matrices with ambiguous Figma naming break the agent's diff. When the Figma file uses State=Loading in one variant and state=loading in another, the agent stumbles. I fix this manually. A stricter Figma edit checklist would prevent it.
  • Screenshots of multi-state previews still need human review. The agent can generate the DOM, but it can't see the rendered pixels without a preview tool, and preview tools are still flakey across repos.
  • DESIGN.md becomes a bottleneck with more than one human editor. The file is the contract, so simultaneous edits create merge conflicts on rules, not on code. The next version of this system needs a structured format – probably YAML or a typed schema – so that rules can be merged mechanically.

None of these are reasons not to do it. All of them are directions for version two.

Reflection

Two things I'm taking from this.

The role shifts from producer to curator. Solo design-engineering with AI agents isn't "the agents design and I approve." It's the opposite: I write the rules, and the agents execute them. The unit of craft stops being the artifact – the Figma frame, the React component, the CSS file – and starts being the contract. A well-written rule in DESIGN.md is worth more than a pixel-perfect Figma frame, because the rule compounds across every future decision.

What I'd do differently.

  • Start the accessibility test suite on day one, not day ten. It catches agent regressions the way linting catches human ones.
  • Build a visual-diff tool between the Figma library and the coded spec pages from the beginning. Drift is easier to prevent than to clean up.
  • Write more quotable rules. Agents execute better against "one value, no exceptions" than against "consider using a single radius value where appropriate."

Factory is a few-days experiment. The next version is a longer one, where the markdown contract governs not just tokens and components but product motion, copy, and metrics.

← Back to all workNext projectRedesigning EF Global Navigation →