Skip to content

Build frontend UI from designs with Cursor

Cursor builds frontend UI from designs when its agent gets three inputs: the design itself (a Figma frame through Figma’s MCP server, or a cropped screenshot), a project rule that pins the design-system tokens and components, and a check loop. Playwright screenshot comparisons, axe accessibility scans and the type checker then prove the result matches.

This page is for the frontend developer who turns designs into components, and for the tech lead who owns the component library. The designer hands you 15 screens for a SaaS dashboard. The agent’s first attempt looks right at 1440 pixels, collapses at 375, uses three hex colors that are not in your palette, and writes a new button instead of importing yours. You can catch that by reading every line of JSX, or you can make the checks catch it.

Cursor details on this page were checked on 2026-09-26 against Figma’s MCP README and @cursor/sdk 1.0.32; UI labels can differ in your Cursor build.

What this design-to-code workflow gives you

Section titled “What this design-to-code workflow gives you”
  • A .cursor/mcp.json that connects Cursor’s agent to Figma and to a real browser.
  • A design-system rule that makes the agent reuse your tokens and components instead of inventing new ones.
  • A Playwright test that checks each component at three widths, in light and dark mode, with an accessibility scan.
  • Three copy-paste prompts: implement a Figma frame, implement from a screenshot, and close the visual gap in a loop.
  • A sign-off split in which the designer approves screenshots once and CI enforces them from then on.

The input decides how much the agent guesses. Give it the most exact source you can.

InputWhat the agent receivesUse it whenLimit
Figma’s remote MCP server (https://mcp.figma.com/mcp)Layout, variables, components, Code Connect mappings and a rendered screenshot of the frameThe design lives in Figma and you have a Dev or Full seatStarter plans, and View or Collab seats on paid plans, get “up to 6 tool calls per month” (Figma README, checked 2026-09-26)
Community figma-developer-mcp 0.13.2 (GLips/Figma-Context-MCP)Read-only layout and style data through a personal Figma API keyNo Dev seat, but you can create an API keyRead-only; you manage the key (setup in step 2 below)
A cropped screenshot attached to the chatPixels only; spacing and color are estimatesThe design is a PNG, a competitor page, or a whiteboard photoState exact values the image cannot convey: radius, shadow, font weight

Cursor’s agent accepts images alongside a prompt: the user-message type in @cursor/sdk 1.0.32 carries an images field. A screenshot still loses what the MCP server returns as data, so treat screenshots as the fallback.

The agent needs two servers: Figma to read the design, and Playwright to look at what it built. For the full MCP setup, including secrets and troubleshooting, see how to set up MCP servers in Cursor.

  1. Install the Figma plugin. The Figma README recommends it because it bundles the MCP server configuration with skills for implementing designs and Code Connect. Type this in Cursor’s agent chat:

    /add-plugin figma

    If your team cannot install plugins, add the server by hand instead. Open Cursor Settings, go to the MCP tab, choose Add new global MCP server, and save {"mcpServers": {"figma": {"url": "https://mcp.figma.com/mcp"}}}. The server uses OAuth, so the first connection asks you to sign in to Figma. No key goes in the file.

  2. Add Playwright MCP to the project. Create or extend .cursor/mcp.json in the repository root and commit it:

    {
    "mcpServers": {
    "playwright": {
    "command": "npx",
    "args": ["@playwright/mcp@0.0.83", "--isolated"]
    }
    }
    }

    @playwright/mcp is Microsoft’s server (0.0.83 on npm, checked 2026-09-26). The version is pinned because every teammate runs this committed file: bump it deliberately in a pull request, not implicitly with @latest. The --isolated flag keeps the browser profile in memory, so parallel agent sessions do not conflict over one persistent profile (Playwright MCP README: “a persistent profile can only be used by one browser instance at a time”).

    Without a Dev or Full seat, add the community Figma server to the same file instead of the official one. The entry passes the key through the server’s env option, as the GLips/Figma-Context-MCP README documents. The key itself is never written into the file: Cursor’s MCP docs describe ${env:NAME} interpolation in mcp.json (secondary source; not re-verified on 2026-09-26). Check this in your build. If the server reports a missing key, export FIGMA_API_KEY in your shell before you launch Cursor:

    {
    "mcpServers": {
    "framelink-figma": {
    "command": "npx",
    "args": ["-y", "figma-developer-mcp@0.13.2", "--stdio"],
    "env": { "FIGMA_API_KEY": "${env:FIGMA_API_KEY}" }
    }
    }
    }

    The key can read every file your Figma account can read, so treat it like any other token; the Figma MCP guide covers the trade-offs.

  3. Check that the servers are listed. If the new servers do not appear under Cursor Settings → MCP, restart Cursor fully and check again.

  4. Prove both servers answer. Paste this into the agent chat:

    List every tool you can call from the figma and playwright MCP servers, one line each.
    Then call the playwright navigate tool on http://localhost:3000 and tell me the page title.
    Do not guess: if a server is missing, say which one.

    A correct answer names tools such as get_design_context and get_screenshot for Figma and browser_navigate for Playwright, and reports the real title of your running dev server.

Claude Code and Codex connect to the same Figma server with their own commands; the Figma MCP guide has all three.

Without a rule, every generated component picks its own spacing and colors. The rule should point at the files that hold your tokens, not copy their values. Copied hex values drift the first time a designer changes the palette, and then the rule contradicts the code.

Save this as .cursor/rules/design-system.mdc. The frontmatter keys (description, globs, alwaysApply) match the Cursor rule in Figma’s MCP README. The rule uses a single glob, because the syntax for a list of globs in .mdc files is not verified. After you save the rule, confirm that it attaches with the canary check from project rules in Cursor.

---
description: Design-system tokens, components and UI conventions for React code
globs: src/**/*.tsx
alwaysApply: false
---
- Tokens live in src/styles/tokens.css (CSS variables) and tailwind.config.ts. Use only those.
Never write a raw hex, rgb() or px value in a component. If a value is missing, stop and ask.
- Reuse components from src/components/ui (Button, Card, Input, Dialog, Table, Skeleton)
before writing new markup. Never create a second button or card.
- Every interactive element has a visible focus state and an accessible name.
- Every color class works in light and dark mode.
- Mobile first: base classes target 375px, then md: and lg: add layout.
- When the Figma MCP server returns a localhost URL for an image or SVG, use it directly.
Do not add icon packages and do not create placeholders.
- Never run Playwright with --update-snapshots. A changed screenshot is reported, not accepted.

The Figma README ships an equivalent asset-handling rule; the plugin from step 1 installs it for you. The last line protects your checks, which the next section sets up. For the reasoning, see how to keep agents from editing the oracle.

Write the visual check before the component

Section titled “Write the visual check before the component”

Write the acceptance check first, so “done” means a test passes and not “the agent says it matches”. Render the component on a fixture route (or a Storybook story) with fixed data, then check it at three widths in both color schemes, and run an axe scan on each width and scheme.

Install the two packages once (in the terminal):

Terminal window
npm install -D @playwright/test @axe-core/playwright
npx playwright install chromium

Save this as tests/visual/resource-table.visual.spec.ts. It assumes baseURL points at your dev server in playwright.config.ts.

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const viewports = [
{ name: 'mobile', width: 375, height: 812 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 800 },
];
for (const vp of viewports) {
for (const scheme of ['light', 'dark'] as const) {
test(`resource table: ${vp.name}, ${scheme}`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.emulateMedia({ colorScheme: scheme });
await page.goto('/dev/fixtures/resource-table');
await expect(page.getByRole('table')).toBeVisible();
await expect(page).toHaveScreenshot(`resource-table-${vp.name}-${scheme}.png`, {
animations: 'disabled',
mask: [page.getByTestId('relative-time')],
maxDiffPixelRatio: 0.01,
});
const { violations } = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(violations).toEqual([]);
});
}
}

The snippet was type-checked against @playwright/test 1.63.0 and @axe-core/playwright 4.13.0 on 2026-09-26. emulateMedia switches the color scheme only if your Tailwind dark mode follows the media query; with the class strategy, add the dark class to <html> in the fixture instead.

A screenshot test has no baseline on its first run. That first set of images is the moment a human judges the match: the designer compares the six PNGs with the Figma frames and approves them in the pull request. From then on CI rejects any drift above 1% of pixels, and nobody has to look again until the design changes.

With the servers, the rule and the check in place, the prompt can be short and specific. Copy the frame link from Figma with the frame selected, so the URL carries a node-id.

Replace FILE_KEY and the node-id with your frame’s link. One frame per prompt works better than a whole page: the MCP response for a full screen is large, and the agent’s attention spreads across it.

For a whole screen, start in Cursor’s Plan Mode, which creates an implementation plan before it writes code. Ask for the component breakdown first and implement the frames one at a time.

Implement from a screenshot when there is no Figma file

Section titled “Implement from a screenshot when there is no Figma file”

A screenshot cannot show a mobile layout that only exists in the designer’s head, so write the responsive behaviour into the prompt.

The first render is usually close but not exact. Instead of describing each difference yourself, let the agent compare the design with the running page.

The three-round cap matters: without it, the agent keeps adjusting sub-pixel differences that the 1% threshold already tolerates. For a single class change, selecting the JSX and describing the edit is faster; see inline edit strategies.

How do you know the UI is done without reading every line?

Section titled “How do you know the UI is done without reading every line?”

The component is done when these checks pass, and the reviewer reads their output rather than the markup:

CheckCatchesRuns whereWho signs off
npx tsc --noEmitWrong prop types, missing states in a unionAgent’s loop and CICI
Your linter with jsx-a11y rulesMissing labels, click handlers on non-interactive elementsAgent’s loop and CICI
rg -n '#[0-9a-fA-F]{3,8}\b' src/components; test $? -eq 1 (only rg’s “no match” exit code 1 passes, so a match or a tool error, such as a missing rg binary, fails the build)Raw hex values that bypass tokensCICI
Playwright toHaveScreenshot, 3 widths × 2 schemesLayout drift, broken breakpoints, dark-mode regressionsCIDesigner approves the baseline once; CI enforces it
axe scan (wcag2a, wcag2aa)Contrast, roles, namesCICI; a deliberate exception needs a named reviewer
Component tests for statesLoading, empty and error behaviourCIDeveloper writes the cases before the prompt

In the pull request, the reviewer looks at the screenshot diff that Playwright’s HTML report attaches, the axe result, and the list of files touched. A pull request that changes a baseline PNG needs the designer’s approval; enforce that with a CODEOWNERS entry for the snapshot folder. Reading evidence instead of code explains the wider practice, and visual acceptance before implementation moves the designer’s check even earlier.

What breaks when Cursor builds UI from designs?

Section titled “What breaks when Cursor builds UI from designs?”

The agent writes raw hex values or new spacing. The rule did not load, usually because a glob does not match the file the agent edited. Recovery: add a canary line to the rule and check that the agent repeats it, as shown in project rules in Cursor. Keep the rg hex check in CI so a missed rule fails the build instead of shipping.

The agent builds a second button instead of importing yours. It did not know the component exists. Recovery: name the components in the rule and in the prompt. If your team uses Figma Code Connect, map the Figma components to your code components so get_design_context returns the import.

Figma calls stop after a handful of requests. Starter plans, and View or Collab seats on paid plans, are capped at six tool calls a month. Recovery: implement from a screenshot of the frame, or move the implementer to a Dev seat; the community figma-developer-mcp server from step 2 is the read-only alternative.

Icons turn into a new icon package or grey placeholders. The agent ignored the asset URLs the Figma server returned. Recovery: make sure the Figma asset rule is installed (the plugin adds it), and reject any package.json change in a UI pull request that you did not ask for.

Screenshot tests pass locally and fail in CI. Fonts and anti-aliasing differ between macOS and the Linux runner. Recovery: generate and compare baselines in the same container image that CI uses, keep animations: 'disabled', and mask timestamps, avatars and other live data.

The agent “fixes” a failing screenshot test by updating the snapshot. The baseline is the oracle, and the agent changed it. Recovery: revert the PNG, keep the no-update line in the rule, and protect the snapshot folder with CODEOWNERS, so a baseline change always reaches the designer.

The layout breaks between breakpoints. The design only showed 1440 pixels, and the agent guessed the rest. Recovery: ask for the mobile frame, or write the behaviour per breakpoint into the prompt as in the screenshot prompt above. If the design uses a custom breakpoint, add it to tailwind.config.ts once instead of using arbitrary values.

The hex check fails on something that is not a color. The pattern is deliberately broad, because Tailwind arbitrary values such as bg-[#1a2b3c] are the case it must catch, so it also matches a fragment link made only of hex letters (href="#add") and an issue number in a comment (#123). Recovery: give the fragment id a non-hex first letter (#new-row), and link issues by URL instead of #123. Do not loosen the pattern until it stops seeing [#.

The component grows past 300 lines. One prompt asked for layout, data, sorting and state together. Recovery: ask the agent to extract the header, the row and the sort logic into a hook, and to keep the visual test green after each extraction.