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.jsonthat 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.
Which design input should the agent get?
Section titled “Which design input should the agent get?”The input decides how much the agent guesses. Give it the most exact source you can.
| Input | What the agent receives | Use it when | Limit |
|---|---|---|---|
Figma’s remote MCP server (https://mcp.figma.com/mcp) | Layout, variables, components, Code Connect mappings and a rendered screenshot of the frame | The design lives in Figma and you have a Dev or Full seat | Starter 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 key | No Dev seat, but you can create an API key | Read-only; you manage the key (setup in step 2 below) |
| A cropped screenshot attached to the chat | Pixels only; spacing and color are estimates | The design is a PNG, a competitor page, or a whiteboard photo | State 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.
Connect Cursor to Figma and a browser
Section titled “Connect Cursor to Figma and a browser”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.
-
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 figmaIf 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. -
Add Playwright MCP to the project. Create or extend
.cursor/mcp.jsonin the repository root and commit it:{"mcpServers": {"playwright": {"command": "npx","args": ["@playwright/mcp@0.0.83", "--isolated"]}}}@playwright/mcpis 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--isolatedflag 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
envoption, 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 inmcp.json(secondary source; not re-verified on 2026-09-26). Check this in your build. If the server reports a missing key, exportFIGMA_API_KEYin 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.
-
Check that the servers are listed. If the new servers do not appear under Cursor Settings → MCP, restart Cursor fully and check again.
-
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_contextandget_screenshotfor Figma andbrowser_navigatefor 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.
Pin the design system in a project rule
Section titled “Pin the design system in a project rule”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 codeglobs: src/**/*.tsxalwaysApply: 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):
npm install -D @playwright/test @axe-core/playwrightnpx playwright install chromiumSave 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.
Implement a Figma frame with the agent
Section titled “Implement a Figma frame with the agent”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.
Close the last visual gap in a loop
Section titled “Close the last visual gap in a loop”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:
| Check | Catches | Runs where | Who signs off |
|---|---|---|---|
npx tsc --noEmit | Wrong prop types, missing states in a union | Agent’s loop and CI | CI |
Your linter with jsx-a11y rules | Missing labels, click handlers on non-interactive elements | Agent’s loop and CI | CI |
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 tokens | CI | CI |
Playwright toHaveScreenshot, 3 widths × 2 schemes | Layout drift, broken breakpoints, dark-mode regressions | CI | Designer approves the baseline once; CI enforces it |
axe scan (wcag2a, wcag2aa) | Contrast, roles, names | CI | CI; a deliberate exception needs a named reviewer |
| Component tests for states | Loading, empty and error behaviour | CI | Developer 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.