Skip to content

The 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.

  • 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.

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.

SituationUse
New customer-facing flow, several states, more than one breakpointAll four artifacts
New screen built only from existing componentsSkip stage 1; start at the prototype
Internal tool, established component libraryA single visual acceptance artifact and the step 5 gates
Copy or spacing fixNormal 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”
StageReadsProducesAccepted byEvidence kept
1. Visual directionintent.md, brand constraints, referencesTwo or three annotated directionsProduct and designChosen direction, rejected ones with reasons
2. Interaction prototypeAccepted directionPrototype plus a state matrixProduct and designStable link or committed prototype, state matrix
3. Design contractPrototype, design-system sourcedesign/<feature>/contract.yamlDesign-system ownerContract version, token source path
4. ImplementationContract, spec.md, plan.mdCode, tests, screenshot baselinesCode ownerPassing 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.

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.

Terminal window
# Figma: plugin bundles the MCP server and Figma's skills
claude plugin install figma@claude-plugins-official
# or the server alone, then run /mcp in a session to log in
claude mcp add --transport http figma https://mcp.figma.com/mcp
# Playwright; --isolated keeps parallel agents from sharing one browser profile
claude mcp add playwright -- npx @playwright/mcp@latest --isolated

Reference exported frames and screenshots by path in the prompt; Claude Code reads image files directly.

Steps 1 to 3 produce the first three artifacts. Steps 4 to 6 together produce the fourth, the implementation with its proof.

  1. 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).

  2. 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.

  3. 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-contract
    version: 3
    source: https://www.figma.com/design/FILE_KEY/Checkout?node-id=12-345 # version-pinned link
    tokens_source: src/styles/tokens.css
    components:
    - name: CheckoutSummary
    reuses: [Card, Button, Alert]
    states: [ready, empty, loading, error, card-declined, permission-denied]
    states_doc: design/checkout/states.md
    breakpoints: [375, 1280]
    accessibility: "no serious or critical axe violations; focus order as in states.md"
    new_tokens: [] # any entry needs design-system owner approval
    acceptance_owner: design-system lead

    FILE_KEY is your Figma file’s key. An empty new_tokens list is the default; the design-system owner signs off on every addition before implementation starts.

  4. Plan, then implement against the contract.

    Hand contract.yaml and states.md to the planning step so they shape spec.md and plan.md, and review the plan before any code is written: /plan in Claude Code and Codex, Plan Mode in Cursor. Implement on an isolated branch or worktree, and have the agent write the step 5 tests from states.md before the component. The tests need a way to put the real app into each state, so have the agent expose the states.md fixtures to the test build only, for example a fixture query parameter honoured only when NODE_ENV is test, or route mocking with page.route, and list that hook in plan.md.

  5. 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/playwright 4.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 of states.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_URL to 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 summary
    const 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. rg exits 1 only when nothing matches, so the gate passes on exit 1 alone: a match (exit 0), an rg error such as a bad path (exit 2), or a missing rg (exit 127) all fail the CI step. A bare ! rg … would pass on the last two. The || s=$? keeps a non-zero rg exit 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 as red; for those, add a stylelint rule such as color-named: never. An occasional false positive, such as a #add anchor, 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.

  6. 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.md and run it without an interactive session:

    Terminal window
    claude -p --permission-mode dontAsk \
    --allowedTools "Read Grep Glob mcp__playwright" \
    < design/checkout/review-prompt.md

    The prompt goes in on stdin because --allowedTools takes several values: a prompt placed after it would be read as one more tool name (checked on Claude Code 2.1.283).

    dontAsk denies 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.

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.

CheckWhat it provesWho owns it
State-matrix Playwright specEvery agreed state renders at every agreed viewportCode owner
Screenshot baselinesNothing visual changed without approvalDesign (approves baseline updates)
axe scan, focus assertionsNo serious or critical accessibility violations; keyboard path worksCode owner, accessibility owner where you have one
Token gateNo colours outside the token sourceDesign-system owner
Fresh-context review reportMismatches against the accepted frames, each with evidenceProduct 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.

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.

  • decisions.md records the chosen direction and why the others lost.
  • states.md has a fixture and an acceptance line for every state at every viewport.
  • contract.yaml points at the canonical token source and lists no unapproved new_tokens.
  • The Playwright spec covers every row of states.md and 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.