Four-artifact design-to-code pipeline
PLThe four-artifact design-to-code pipeline moves a user interface from intent to production through four versioned artifacts: a visual direction, an interaction prototype, a design contract, and an implementation with proof. Each artifact has a named owner who accepts it, and the last one is checked by state tests, screenshot baselines, and accessibility scans rather than by reading every line.
You asked an agent for a checkout summary, it produced a polished screen in ten minutes, and the next two weeks went on the parts nobody specified: the empty cart, the declined card, the keyboard path, and colours that exist nowhere in your design system. This page is for developers and tech leads who ship frontend work with Claude Code, Codex, or Cursor and want those decisions made before the code, and checked automatically after it.
What the four-artifact pipeline gives you
Section titled “What the four-artifact pipeline gives you”- A prompt for each stage, from three genuinely different visual directions to an evidence-only design review.
- A design contract file that pins components, states, tokens, and breakpoints to your existing design system.
- A Playwright spec pattern that turns the contract’s state matrix into screenshot and accessibility gates.
- A sign-off table that says who accepts which artifact, so the agent never approves its own work.
- Recovery steps for the five ways this pipeline usually breaks.
When is the pipeline worth the overhead?
Section titled “When is the pipeline worth the overhead?”Match the artifact cost to the design risk. Four gated artifacts pay off when a wrong guess means rework across several screens; for a small change they are ceremony.
| Situation | Use |
|---|---|
| New customer-facing flow, several states, more than one breakpoint | All four artifacts |
| New screen built only from existing components | Skip stage 1; start at the prototype |
| Internal tool, established component library | A single visual acceptance artifact and the step 5 gates |
| Copy or spacing fix | Normal change, screenshot baseline update approved in review |
The pipeline covers the Design stage of the lifecycle and hands over to planning. It assumes you already have an accepted intent.md; the design contract then feeds spec.md and plan.md in the artifact chain.
What each stage reads, produces, and who signs off
Section titled “What each stage reads, produces, and who signs off”| Stage | Reads | Produces | Accepted by | Evidence kept |
|---|---|---|---|---|
| 1. Visual direction | intent.md, brand constraints, references | Two or three annotated directions | Product and design | Chosen direction, rejected ones with reasons |
| 2. Interaction prototype | Accepted direction | Prototype plus a state matrix | Product and design | Stable link or committed prototype, state matrix |
| 3. Design contract | Prototype, design-system source | design/<feature>/contract.yaml | Design-system owner | Contract version, token source path |
| 4. Implementation | Contract, spec.md, plan.md | Code, tests, screenshot baselines | Code owner | Passing gates, review report |
Keep one canonical artifact per decision. When the source lives in a design tool, link it by a stable version and export the frames reviewers need, because not every reviewer has a seat in that tool.
Connect the design and browser tools
Section titled “Connect the design and browser tools”The pipeline works with plain image files, but two MCP servers remove most of the manual copying: the official Figma MCP server for design context, and Playwright MCP (npm @playwright/mcp) so the agent can open the running app. Both work in all three tools; only the install differs.
# Figma: plugin bundles the MCP server and Figma's skillsclaude plugin install figma@claude-plugins-official# or the server alone, then run /mcp in a session to log inclaude mcp add --transport http figma https://mcp.figma.com/mcp
# Playwright; --isolated keeps parallel agents from sharing one browser profileclaude mcp add playwright -- npx @playwright/mcp@latest --isolatedReference exported frames and screenshots by path in the prompt; Claude Code reads image files directly.
codex mcp add figma --url https://mcp.figma.com/mcpcodex mcp login figma# or the plugin with Figma's skills: codex plugin add figma@openai-curated
codex mcp add playwright -- npx @playwright/mcp@latest --isolatedAttach images to a prompt with -i / --image (interactive and codex exec, 0.157.1). Image generation is on by default in 0.157.1, which is useful for stage 1 mockups.
Add both servers to .cursor/mcp.json in the repository so the whole team gets them:
{ "mcpServers": { "figma": { "url": "https://mcp.figma.com/mcp" }, "playwright": { "command": "npx", "args": ["@playwright/mcp@latest", "--isolated"] } }}This shape comes from the vendors’ READMEs; it was not tested in a Cursor binary for this page. Reference exported frames with @ in the Agent prompt.
Run the pipeline step by step
Section titled “Run the pipeline step by step”Steps 1 to 3 produce the first three artifacts. Steps 4 to 6 together produce the fourth, the implementation with its proof.
-
Generate materially different visual directions.
Ask for alternatives that differ in hierarchy, density, or navigation, not three colour variations of one layout. The agent writes no production code in this stage.
Record the chosen direction and one line per rejected alternative in
design/<feature>/decisions.md. The rejected reasons stop the same idea returning at implementation (step 4). -
Make the behaviour inspectable with a state matrix.
Build the prototype where the risk is: in Figma for system alignment, as a local HTML sandbox for interaction, or inside the app behind fixture data for high fidelity. The prototype is only half of the artifact; the state matrix is the part that becomes tests.
Product and design accept the prototype by walking the state matrix, not by looking at the happy path.
-
Extract the design contract from the design-system source.
The contract points at your existing tokens and components instead of inventing new ones. With the Figma MCP server connected, the agent reads variables and components from the frame through
get_design_context; without it, it reads the exported frames and your token file.design/checkout/contract.yaml artifact: design-contractversion: 3source: https://www.figma.com/design/FILE_KEY/Checkout?node-id=12-345 # version-pinned linktokens_source: src/styles/tokens.csscomponents:- name: CheckoutSummaryreuses: [Card, Button, Alert]states: [ready, empty, loading, error, card-declined, permission-denied]states_doc: design/checkout/states.mdbreakpoints: [375, 1280]accessibility: "no serious or critical axe violations; focus order as in states.md"new_tokens: [] # any entry needs design-system owner approvalacceptance_owner: design-system leadFILE_KEYis your Figma file’s key. An emptynew_tokenslist is the default; the design-system owner signs off on every addition before implementation starts. -
Plan, then implement against the contract.
Hand
contract.yamlandstates.mdto the planning step so they shapespec.mdandplan.md, and review the plan before any code is written:/planin Claude Code and Codex, Plan Mode in Cursor. Implement on an isolated branch or worktree, and have the agent write the step 5 tests fromstates.mdbefore the component. The tests need a way to put the real app into each state, so have the agent expose thestates.mdfixtures to the test build only, for example afixturequery parameter honoured only whenNODE_ENVistest, or route mocking withpage.route, and list that hook inplan.md. -
Prove the implementation in the real application.
The state matrix becomes a Playwright spec: one test per state, run at both viewports, each asserting a screenshot baseline, keyboard focus, and an axe scan (
@axe-core/playwright4.13.0). The viewports come from two Playwright projects, so declare them in the config; without it the spec runs at one viewport and covers half ofstates.md:playwright.config.ts import { defineConfig } from '@playwright/test';export default defineConfig({testDir: 'e2e',use: { baseURL: process.env.BASE_URL ?? 'http://localhost:3000' },projects: [{ name: 'mobile-375', use: { viewport: { width: 375, height: 812 } } },{ name: 'desktop-1280', use: { viewport: { width: 1280, height: 800 } } },],});Playwright adds the project name to each screenshot file name automatically, so every state keeps one baseline per viewport. Set
BASE_URLto your dev server.e2e/checkout-summary.spec.ts import { test, expect } from '@playwright/test';import AxeBuilder from '@axe-core/playwright';// First focusable element per state (accessible name), from the focus// order in states.md; null means the state has nothing focusable.const firstFocus: Record<string, string | null> = {ready: 'Pay now',empty: 'Browse products',loading: null,error: 'Try again','card-declined': 'Use another card','permission-denied': 'Sign in',};for (const [state, name] of Object.entries(firstFocus)) {test(`checkout summary: ${state}`, async ({ page }) => {await page.goto(`/checkout?fixture=${state}`);const summary = page.getByTestId('checkout-summary');await expect(summary).toHaveScreenshot(`summary-${state}.png`);if (name !== null) {// Tab past the header and skip links until focus enters the summaryconst focused = summary.locator(':focus');for (let i = 0; i < 20 && (await focused.count()) === 0; i++) {await page.keyboard.press('Tab');}await expect(focused).toHaveAccessibleName(name); // toHaveAccessibleName: Playwright 1.44+}const { violations } = await new AxeBuilder({ page }).include('[data-testid="checkout-summary"]').analyze();expect(violations.filter((v) => v.impact === 'serious' || v.impact === 'critical')).toEqual([]);});}Add a token gate so no raw colour slips past the contract.
rgexits 1 only when nothing matches, so the gate passes on exit 1 alone: a match (exit 0), anrgerror such as a bad path (exit 2), or a missingrg(exit 127) all fail the CI step. A bare! rg …would pass on the last two. The|| s=$?keeps a non-zerorgexit from stopping the step early under GitHub Actions’bash -e:Terminal window s=0; rg -n -e '#[0-9a-fA-F]{3,8}\b' -e '\b(rgb|hsl)a?\(' --glob '*.{css,ts,tsx}' src/components/checkout/ || s=$?[ "$s" -eq 1 ] || { echo 'raw colour found, or rg failed'; exit 1; }The pattern catches hex values and
rgb()/hsl()calls, not named colours such asred; for those, add a stylelint rule such ascolor-named: never. An occasional false positive, such as a#addanchor, is fixed by moving the value into a token or narrowing the glob.The first run creates the baselines. A designer reviews those images once against the prototype; from then on, any pixel change fails the test until a human approves the new baseline.
-
Review the result in a fresh context.
A reviewer that did not build the component compares the running implementation with the accepted artifacts and reports only mismatches it can show. Give it read-only permissions.
Replace
<DEV_URL>with the URL of your own dev server. In a setup with parallel worktrees the default port can belong to another agent’s server.Save the prompt as
design/checkout/review-prompt.mdand run it without an interactive session:Terminal window claude -p --permission-mode dontAsk \--allowedTools "Read Grep Glob mcp__playwright" \< design/checkout/review-prompt.mdThe prompt goes in on stdin because
--allowedToolstakes several values: a prompt placed after it would be read as one more tool name (checked on Claude Code 2.1.283).dontAskdenies every tool call that would prompt and is not in--allowedTools, so the reviewer can read files and drive the browser but cannot edit.Or define a read-only custom subagent for design review and call it at the end of each implementation session.
Terminal window codex exec -s read-only \-i design/checkout/accepted/summary-375.png \-i design/checkout/accepted/summary-1280.png \< design/checkout/review-prompt.mdThe prompt comes from stdin because
-iaccepts several files and would otherwise read a trailing prompt as another image path.Start a new Agent chat (not the one that built the component), paste the prompt, and
@-referencecontract.yaml,states.md, and the accepted frames. Keep the Playwright MCP server enabled so the reviewer can open the app.
How do you verify a UI without reading every line?
Section titled “How do you verify a UI without reading every line?”You review the artifacts and the gates, not the diff. The contract and the state matrix are short enough for a human to read in full; the code is checked by machines against them.
| Check | What it proves | Who owns it |
|---|---|---|
| State-matrix Playwright spec | Every agreed state renders at every agreed viewport | Code owner |
| Screenshot baselines | Nothing visual changed without approval | Design (approves baseline updates) |
| axe scan, focus assertions | No serious or critical accessibility violations; keyboard path works | Code owner, accessibility owner where you have one |
| Token gate | No colours outside the token source | Design-system owner |
| Fresh-context review report | Mismatches against the accepted frames, each with evidence | Product and design accept or reject each finding |
Make baselines hard to change silently: put the snapshot directory under a CODEOWNERS entry owned by design, and treat an agent-run npx playwright test --update-snapshots as a failed task. The broader rule is on protecting the oracle from the agent. Collect the passing run, the review report, and the approved baselines into the PR’s evidence bundle.
What breaks in a design-to-code pipeline?
Section titled “What breaks in a design-to-code pipeline?”The agent updates the screenshot baselines to make the tests pass. Every test is green and the UI has drifted. Recover by reverting the snapshot directory to the last approved commit, rerunning the suite, and adding the CODEOWNERS rule above so the next update needs a design approval.
The contract invents tokens. The implementation introduces #1f6feb because the prototype used it. Recover by running the token gate, mapping each raw value to the nearest existing token or to an approved new_tokens entry, and redoing the design contract (stage 3) if the prototype itself left the design system.
The screen matches one screenshot but not the flow. The happy path looks right; the declined card shows a blank panel. Recover by checking states.md for the missing row, adding the fixture, and letting the failing test drive the fix. A state with no fixture does not exist as far as the gates are concerned.
Baselines flicker between runs. Fonts, dates, or animations change pixels and the suite becomes noise that people learn to ignore. Recover by freezing the clock and data in the fixture, waiting for web fonts before the screenshot, and masking genuinely dynamic regions with the mask option of toHaveScreenshot. Do not raise the pixel tolerance until the test no longer detects anything.
A design tool or connector stops being available. A seat changes, a beta ends, or the data boundary blocks a provider. The artifacts in design/<feature>/ still carry every decision, so rerun only the stage that depended on the tool, from exported files.
Check the handoffs before you merge
Section titled “Check the handoffs before you merge”-
decisions.mdrecords the chosen direction and why the others lost. -
states.mdhas a fixture and an acceptance line for every state at every viewport. -
contract.yamlpoints at the canonical token source and lists no unapprovednew_tokens. - The Playwright spec covers every row of
states.mdand passes in CI. - Baseline changes in the PR were approved by design, not generated by the agent.
- The fresh-context review report has no open blockers.
Where to go next from the design pipeline
Section titled “Where to go next from the design pipeline”- Score each handoff with the evidence-based rating loop when an artifact keeps coming back.
- Turn
states.mdrows into executable acceptance criteria for non-visual behaviour. - Continue through Build and Test in the lifecycle.
- Set up browser tooling in depth with browser automation for agents.