Pixelwright docs
Quick start
Requires Node.js 24.11 or newer and Figma Desktop. Run the setup in a terminal:
npx -y pixelwright@latest
It connects Claude Code and Cursor, shows what to paste for Codex and other MCP clients, and puts the Figma plugin in
~/.pixelwright/plugin. Then, in Figma Desktop:
- Plugins → Development → Import plugin from manifest… and pick
~/.pixelwright/plugin/manifest.json, once. Figma loads the plugin from there every time you run it, and the setup puts the new one there on every update. (The plugin is not on the Figma Community: Figma does not list plugins that overlap its own MCP server.) - Open your file and run Plugins → Development → Pixelwright. Keep its small window open while you work.
- The first time, the window asks for a pairing code: your agent shows it (from
status); type it in. - Ask your agent to read a section, or turn on Allow edits in the window and ask it to change something.
npx -y pixelwright@latest setup --print asks nothing: it puts the plugin in place and prints the commands and config
blocks, for scripts.
Try it on the demo
Open the Orbit demo in Figma: a small pricing site made for Pixelwright. Duplicate it to your drafts, run the plugin in your copy and follow its Start here page, which names the layer each tool shows best: three breakpoints, an A/B hero, a prototype flow, Dev Mode notes and measurements, Motion, components with an exposed instance and a slot, and the deliberate mistakes the audits find. examples/orbit is the code an agent builds from it: the tokens, the components, the Code Connect files and a baseline.
How it works
Claude Code ──stdio──► pixelwright ──ws://localhost:18720──► Pixelwright plugin ──Plugin API──► your Figma file
The plugin runs inside the file and answers the server on your computer. Every read and write goes through the Figma Plugin API, so nothing reaches api.figma.com and no seat or plan limits the number of calls. Several files can be connected at once; tools work on the one whose plugin window was in front last, or on the file you name.
Code Connect
Pixelwright runs your project's Figma Code Connect templates
(*.figma.ts) and batch files (*.figma.batch.json, one row per component through a shared template), written as
Figma documents them, itself, on any plan, without publishing anything. design_read then gives each connected
instance as $code in the XML with its code and imports, and names the properties its code leaves out:
{ "ids": ["54:268"], "component": "PlanCard", "source": "src/components/PlanCard.tsx",
"snippet": "<PlanCard name=\"Pro\" price=\"$12\" description=\"For freelancers shipping fast.\" popular>\n <Button variant=\"primary\">Get started</Button>\n</PlanCard>",
"imports": ["import { PlanCard } from \"@/components/PlanCard\"", "import { Button } from \"@/components/Button\""] }
A row or a template can name the component's key (components_read gives it), which matches exactly that component
in every file, a library's included. components_read mapped=false lists the components still without code, so a
design system is mapped once, a row each. See examples/orbit
for two templates, one rendering the other inside it, a batch file, and how they match. The project folder is CLAUDE_PROJECT_DIR, else the folder your MCP client starts the server in.
Safety
- Allow edits in the plugin window is off until you turn it on for a file; while it is off, the plugin refuses every write, whatever the agent asks. Your choice is remembered per file on your computer.
- Each tool call that writes is one undo step in Figma (⌘Z / Ctrl+Z).
design_applyandtokens_applytakedryRunto check a change without writing it. - The server listens on localhost only, and a plugin pairs once with a code you type in; unpaired windows get nothing.
executeruns plugin code the agent writes, and Code Connect templates run as your project's code: use Pixelwright in projects and with agents you trust, as you would their build scripts.
Limits
- The file must be open in Figma Desktop with the plugin's window open; writing needs edit access to it.
- Figma takes images up to 4096 px a side, in PNG, JPG or GIF.
- A library's component the file does not use yet is placed by its key (
component="key:…"): the Plugin API cannot search a library's components. - Tested on macOS. On Windows the setup gives Claude Code
cmd /c npx …, as its documentation asks; Windows itself is not tested yet.
Commands
pixelwright in a terminal: the setup; started by an MCP client: the MCP server on stdio
pixelwright setup --print no questions: the plugin put in place, the commands and config blocks printed
pixelwright serve the MCP server, however it is started
pixelwright logs follow the logs of every running server on this machine: tool calls, plugins, errors
pixelwright --version
PIXELWRIGHT_HOME moves ~/.pixelwright (the plugin, pairing keys, written-files.json, the shared log in logs/);
CLAUDE_PROJECT_DIR names the project for Code Connect; PIXELWRIGHT_SUPPORT_REMINDERS=off turns off the weekly
support note (see Support); PIXELWRIGHT_LOG_LEVEL=debug adds the bridge's details to the log.
Updating
MCP clients start pixelwright@latest, so a client started again runs the newest server. The plugin is a copy on your
computer: run npx -y pixelwright@latest again (or setup --print) to put the matching one in ~/.pixelwright/plugin.
Figma Desktop loads it the next time you run the plugin. When the plugin's window says the protocol versions differ,
this is the fix.
Every MCP client session runs its own server, and they share one bridge port. When a newer server starts while an older
one (another client's, still open) holds the port, the older one hands the port over and switches its bridge off: its
status then asks you to restart Pixelwright in that client (/mcp in Claude Code). A plugin turned away by an older
server keeps trying, and connects once the newer one holds the port.
Tools
Every tool the server lists, as your agent sees it. Tools marked writes change the file and need Allow edits.
assets_export
Writes files out of the design into dir. nodes: up to 50 layers, one file each (SVG for an icon, PNG at 2x otherwise, or the format and scale given; PDF too). from: every true asset inside one layer, the icons and images design_read would plan. original: the images as they were placed instead of renders (photos at full size, in their own format). The same bytes are written once, a name taken by other bytes gets -2, and a file already in dir is never overwritten with other bytes. A layer that cannot be exported is reported in errors and the rest are still written.
| Parameter | Type | What it is |
|---|---|---|
nodesoptional | string[] | Layers to write, one file each: SVG for an icon, PNG otherwise, unless format says. Give nodes or from. |
fromoptional | string | A layer to write every true asset of (its icons and images, as design_read plans them). Give nodes or from. |
dir | string | The folder to write into; created if missing. Files already there are never overwritten with other bytes. |
formatoptional | "PNG" | "JPG" | "WEBP" | "SVG" | "PDF" | For nodes: the format of every file. WEBP is rendered as PNG by Figma and encoded here. |
scaleoptional | number | For PNG, JPG and WEBP: pixels per Figma pixel, or per CSS px with width. Default 2. |
widthoptional | number | For nodes as PNG, JPG or WEBP: the CSS width the build shows the layer at; the file is width × scale px wide (110 at scale 2: 220 px). |
qualityoptional | integer | For WEBP: 1 to 100. Default 85. |
originaloptional | booleandefault false | Write the images as they were placed (not cropped, filtered or scaled) instead of renders, with their real format. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
baseline_diff
What the designer changed since baseline_save, in design_read's terms: each layer changed (its attributes as [before, after], a text's content as XML), added (with its XML when short), removed or hidden, moved to another parent or reordered; a change made in a component once, with the instances it reached and how many were compared (of); tokens whose value changed in any mode, renamed, newly used or no longer used; icons and images drawn anew (drawing: export them again). Layers are followed by id, a replaced one by its name. It sees what changes_since and Figma's own Changed mark miss: edits made while the plugin was closed, accepted library updates, variable and style values. Without name: every baseline in the project with how much changed; call it when a session starts or the user says the design changed. It reads the file the baseline came from and works while Allow edits is off. Change only the code it names (code lists the files), then call baseline_save with the accept it gives.
| Parameter | Type | What it is |
|---|---|---|
nameoptional | string | One baseline in detail. Without it: every baseline in the project, with how much changed. |
nodeoptional | string | Compare with this layer instead of the baseline's own, e.g. a v2 the designer made beside it. Needs name. |
fileoptional | string | With name: compare in this file (a session from status, or a file name). Without name: only this file's baselines. Default: the file each baseline came from. |
baseline_save
Keeps a layer as you build code from it, so baseline_diff can tell later what the designer changed: its read and the values of the tokens it uses, in every mode, go into .pixelwright/baselines/<name>.json in the project (it holds the design's texts: commit the folder to share it, or leave it out of git). Save right after the design_read you build a screen or section from; name it after your code (pricing, plan-card) and list in code the files you build from it. Once your code has the changes a baseline_diff listed, pass the accept it gave: exactly the design it compared is kept, so what the designer changed after it still shows next time. An existing name is replaced only with accept or replace: true; code alone changes its file list. Writes nothing to Figma, so it works while Allow edits is off.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer the code is built from: a frame, section, component or instance. Default: the one selected layer. Not with accept. |
nameoptional | string | Named after the code: pricing, plan-card. Default: the layer's name as a slug. Required with accept. |
codeoptional | string[] | The project's files built from the layer, relative to the project. Default: the list the baseline already has. |
acceptoptional | string | The accept a baseline_diff answer gave: keeps exactly the design that diff compared. |
replaceoptional | booleandefault false | Start the named baseline over from the layer as it is now. Not with accept. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
breakpoints_find
Finds the screen a layer is on at the page's other widths (Desktop, Tablet, Mobile): the page's frames, and those in its sections, whose names match once size words are left out (Desktop, Mobile, 1440, 375px) and whose children share names with it, narrowest first, each with a score. Compare them with nodes_diff to write the media queries.
| Parameter | Type | What it is |
|---|---|---|
node | string | A screen's id, or a layer's on it. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
changes_since
What changed in the file since the plugin started watching (its window opened) or since an earlier answer's cursor: each layer created, deleted or changed (with the properties), by the user, a collaborator or Pixelwright itself, on the page the user was on. Ask with no cursor once to start, then with the cursor it gives, to read again only what the designer changed. Only pages the user had open while the plugin ran are watched; an edit made while one of Pixelwright's writes runs (or within a second after) counts as Pixelwright's, and changes from code execute left running count as the user's. What changed while the plugin was closed, and token values, baseline_diff tells against a saved baseline.
| Parameter | Type | What it is |
|---|---|---|
cursoroptional | integer | The cursor an earlier answer gave: only what changed after it. Default: all the plugin has seen. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
code_connect_read
Lists the project's Figma Code Connect files (templates *.figma.ts and *.figma.js, batch files *.figma.batch.json, as figma.config.json's globs name them) with what each connects on the current page, or inside node: components with how many instances and how each matched (by key, id or name), and what a trial run on the first gives: its error, the properties its code never reads (unread) and the variant values its mapping misses (values); missing marks a source file that does not exist. Also the components shown there that none connects, with the id and key a new one needs, and what keeps a file out. design_read returns their code as codeComponents. Write them with your file tools as Figma documents them (guide topic code-connect).
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | A layer or page id to look in, e.g. 54:243. Default: the current page. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
components_read
Lists components: scope used (default) gives the ones the instances on the current page, or inside within, come from (a library's among them, with how many instances use each); scope local gives every component defined in the file. Each comes with its key, variants, properties (type, default, options), description, exposed (the nested instances whose properties its instances show: in code they are its props too) and code: the project's Code Connect template or batch row that connects it ({component, source, template, by}), or null. mapped: false lists only the components without code yet: connect each with a batch row keyed by its key (guide topic code-connect). links: true also reads the links people attached to it in Dev Mode (code, stories, docs), on Figma plans that keep them.
| Parameter | Type | What it is |
|---|---|---|
scopeoptional | "local" | "used"default "used" | used: what the page (or within) uses; local: what the file defines. |
withinoptional | string | A layer or page id to look in. Default: the current page (used) or every page (local). |
queryoptional | string | Only components whose name contains this. |
limitoptional | integerdefault 100 | |
mappedoptional | boolean | true: only components a Code Connect file connects; false: only those none connects. Default: all. |
linksoptional | booleandefault false | Also read the links people attached in Dev Mode (Figma's servers, paid plans only). |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
contrast_check
Rates every visible text under a layer for WCAG contrast against what is behind it (the layers below it that cover it, then its parents' fills, see-through ones laid over what is below): the ratio, whether it is large text (24 px, or 18.66 px bold), and whether it passes AA and AAA. A text over an image or a gradient is said, not guessed: check those in a screenshot. Works while Allow edits is off.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer whose texts to check (a section, a screen). Default: the one layer selected in Figma. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
design_applywrites
Writes Pixelwright XML, the format design_read returns, into Figma. An element with id changes that layer: only the attributes it gives, the rest stays as it is (write background="none", width="auto" to hug, display="block" to drop auto layout); under another parent, with before/after or in another order it moves the layer. An element without id creates a layer: Frame, Section, Text, Rectangle, Ellipse, Line; an Instance by component="Name" variant="Prop=Value" props="Label=Text"; a Component (prop and visibleProp on its layers make its properties) or a ComponentSet of Component variants; an Image or Icon from a file (src); copyOf="id" copies a layer and writes the element onto the copy. var(--Name) binds the style or variable of that name; note writes Dev Mode notes; fonts are loaded for you. XML read with design_read and sent back unchanged changes nothing. Each element is written on its own: errors come back with their line and attribute while the rest goes in, and the whole call is one undo step in Figma. The plugin refuses writes while its Allow edits switch is off (dryRun checks without writing). See guide write.
| Parameter | Type | What it is |
|---|---|---|
xml | string | Pixelwright XML, as design_read writes it: an element with id changes that layer (only the attributes it gives), one without id creates a layer. |
parentoptional | string | Where top-level new layers go: a frame's or page's id. Default: the current page, beside its layers. |
dryRunoptional | booleandefault false | Check ids, tokens and fonts without writing; works with Allow edits off. |
versionoptional | string | A name for a version saved in the file's history before writing, for the designer to go back to (not in a dry run, nor when nothing would change). Pass one before a write that changes many layers or tokens. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
design_audit
The scale a layer uses (the selected layer by default): spacing (gaps and paddings), radii, font sizes, line heights and colours, each with how often it is used and whether it is bound to a token; and what is off it: values off the 4 px grid, raw colours equal to or a hair from a token, sizes a pixel apart. Use it to define tokens when a file has none, or to see what the build should round.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer to audit (a screen, a section). Default: the one layer selected in Figma. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
design_read
Reads a layer (a section, a card, a screen) as Pixelwright's XML: Figma's layer types as tags, Image and Icon for true assets, CSS properties as attributes (var(--Name) where Figma binds a style or variable), ids to read or change a layer later. The first text block is the XML; the second is JSON with tokens (their values), textStyles, fonts, assets (written to assetsDir when given), hints on what the code should bind, truncated (ids of branches left empty to fit maxChars: read each on its own), measurements (the designer's Dev Mode measurements in px) and motion (Figma Motion as CSS @keyframes, a layer's summary being $motion in the XML) when there are any, and, where the project has Figma Code Connect templates (.figma.ts) or batch files (.figma.batch.json), codeComponents: the code connected instances render to ({ids, component, template, source, snippet, imports, unread}, the same code once), with imports and codeErrors; such an instance is $code in the XML, without its children: use its code and add what unread names (its own values are its variant and props; read it with node=<its id> for a nested one's); code: false reads it as layers. A layer with position absolute is placed against its parent element: give that parent position: relative. For a big frame, find its sections with outline first. Rebuild the result with the project's own components and tokens.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer's id, e.g. 54:243. Default: the one layer selected in Figma. |
depthoptional | integer | Levels below the layer to read. Default: all. |
assetsDiroptional | string | Folder to write the icons (SVG) and images (PNG at 2x, originals for backgrounds) into, e.g. src/assets; the XML's src paths start with it. Without it the assets are only planned. |
screenshotoptional | booleandefault false | Also return the layer's image, to compare your build with. |
maxCharsoptional | integerdefault 80000 | Longest XML to return; deeper branches past it come back empty with $truncated, to read on their own. |
codeoptional | booleandefault true | Run the project's Code Connect templates, giving connected instances as $code with their code (default); false reads them as layers. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
executewrites
Runs JavaScript in the Pixelwright plugin, for what design_apply and tokens_apply cannot say: the code is the body of an async function given figma (the Plugin API) and console; return gives result (layers come back as {id, type, name}, figma.mixed as "mixed"), and what it logs comes back as logs. What it throws comes back as error, with the logs before it. What it changes during the call is one undo step in Figma. Refused while the plugin's Allow edits switch is off, but code it starts that outlives the call (timers, listeners, work past the timeout) keeps running until the plugin window is closed, the switch notwithstanding. The tool waits at most 25 s; an endless loop, synchronous or of awaits, freezes Figma until the plugin is closed. Prefer the XML and token tools: they keep the file's styles and check what they write.
| Parameter | Type | What it is |
|---|---|---|
code | string | JavaScript run as the body of an async function with figma (the Plugin API) and console: await works, return gives the result. |
timeoutoptional | numberdefault 10 | Seconds to wait for the code: 10 by default, 25 at most. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
file_map
Every page of the file with its top layers (frames, sections, components) and, inside each section, its own, with their types and sizes, in one call: where to start in a big file. Pages load one at a time; past the time a call has, more: true and the rest are left out (outline them one by one).
| Parameter | Type | What it is |
|---|---|---|
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
flow_read
A page's prototype as a map, for an app's routes: where its flows start, its screens (the page's frames, and those in its sections) with their sizes, and every interaction that leaves its layer: from which screen and layer, on what trigger, to which screen (or back, close, a URL), and how it moves ("navigate to Detail; smart animate 300ms ease-out"). A variant switch stays inside its component and is read by design_read as $on-*.
| Parameter | Type | What it is |
|---|---|---|
pageoptional | string | The page's id. Default: the current page. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
fonts_check
The fonts a layer's texts use (the selected layer's by default), by family with the weights used, and where the build gets each: whether Google Fonts serves it, with a ready css2 link for those weights, and the weights it lacks; a family Google Fonts does not have needs its own files. Google Fonts' public catalogue is read once (no key); offline, the fonts are still listed.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer whose texts' fonts to check (a section, a screen). Default: the one layer selected in Figma. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
guide
How to work with Pixelwright and read what it returns. Without a topic: the topics. Topics: workflow (Which tool to use when, from finding a section to its files.); xml (The XML design_read returns: tags, metadata, CSS attributes, read-only attributes.); tokens (How tokens are named and written: styles, variables, modes, aliases, units, libraries.); assets (What becomes a file, in which format, and how files are named.); write (design_apply: changing, creating, copying and moving layers, components, instances and images with the same XML.); code-connect (Figma Code Connect: templates and batch files, keys, how design_read runs them, what the code leaves out, mapping a design system.); email (Turning the XML into an HTML email that renders in Gmail, Outlook and Apple Mail.). Read workflow first in a new task, and xml before building from design_read.
| Parameter | Type | What it is |
|---|---|---|
topicoptional | "workflow" | "xml" | "tokens" | "assets" | "write" | "code-connect" | "email" | The topic to read. Default: the list of topics. |
image_compare
Renders a layer as screenshot does (scale, effects and clip alike) and compares it with a PNG on disk, typically a screenshot of your build at the same size: rmse (0 to 255) and the share of differing pixels over the part both cover, the box around the differences, and the edges: each horizontal (rows, by y) and vertical (columns, by x) edge of the reference matched, shifted by a few px (by: how far the render's is off), missing, or extra in the render. diff writes a PNG of the difference. Take the reference at the render's density (scale 2 for a 2x screenshot). Works while Allow edits is off.
| Parameter | Type | What it is |
|---|---|---|
node | string | The layer's id, e.g. 54:243. |
reference | string | The PNG to compare with, e.g. a screenshot of your build at the same size: a path, relative to the server's folder or absolute. |
scaleoptional | numberdefault 1 | Pixels per Figma pixel of the render: the reference's density. |
effectsoptional | booleandefault true | Render shadows and outside strokes around the layer; false renders only its own box. |
clipoptional | object | Only this region of the layer, in Figma px from its box's top left (before scale); with effects on it may reach into what paints around the box. |
modesoptional | object | Render in these variable modes, by collection and mode name, e.g. {"Theme": "Dark"}: set on the layer for the render, then set back (needs Allow edits; the file is left as it was). |
diffoptional | string | A .png path to write the difference to: the render faded, every differing pixel red. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
inspect
Reads layers as Figma has them, for what design_read's CSS cannot say, without running code (works while Allow edits is off): the layout box and the painted box (centred or outside strokes, shadows and blur reach past it), the fill as a CSS value with the paint style or variables behind it (what a var() in design_read resolves to), how strokes paint (align, per-side weights, dashes, whether auto layout counts them), every effect with Figma's own parameters (a progressive blur's start and end, noise, texture), a text's lines with what ended each (Enter or Shift+Enter) and its line heights (px, auto, percent), how far a layer overflows its parent, its prototype interactions (trigger, destination, transition, duration, easing) and its component's variant. Boxes are measured from relativeTo's top left, so it also measures: give a list of layers and their section.
| Parameter | Type | What it is |
|---|---|---|
nodesoptional | string[] | The layers' ids, e.g. ["54:243"]. Default: the layers selected in Figma. |
relativeTooptional | string | A layer (a section, a frame) whose top left the boxes are measured from. Default: the page's. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
measure
Measures the distances between layers, each from the one before it in the list (a→b, b→c, so a row's rhythm shows): the gap between their boxes on each axis (negative where they overlap), how their edges and centres line up (B's minus A's: 0 is aligned), and, when one holds the other, how far it sits from each edge, as padding. Layout boxes by default; painted: true measures what is painted (strokes, shadows). A sentence per pair says it plainly. Works while Allow edits is off.
| Parameter | Type | What it is |
|---|---|---|
nodesoptional | string[] | The layers' ids in the order to measure, each from the one before, e.g. ["54:244", "54:268"]. Default: the layers selected in Figma. |
paintedoptional | booleandefault false | Measure what is painted (centred or outside strokes, shadows, blur) instead of the layout boxes. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
node_show
Brings layers into view in Figma (switching to their page when needed) and selects them, so the user sees what you mean: one layer, or several on one page, fitted in view or at a zoom (1 is 100%) centred on them; select: false only moves the view. Changes only the view and the selection, never the file.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer's id, e.g. 54:243. Give node or nodes. |
nodesoptional | string[] | Several layers on one page, fitted in view together. |
zoomoptional | string | numberdefault "fit" | "fit" (default), or a zoom: 1 is 100%, 0.5 is 50%. |
selectoptional | booleandefault true | Select them too (default), or only bring them into view. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
nodes_deletewrites
Deletes layers by id, each on its own: one Figma will not delete (a layer inside an instance, which comes from its component: hide it with display="none" in design_apply instead) stays, with why. One call is one undo step in Figma. Refused while the plugin's Allow edits switch is off.
| Parameter | Type | What it is |
|---|---|---|
nodes | string[] | The layers' ids, e.g. ["54:243"]. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
nodes_diff
Compares two layers (A/B variants of a screen, a card and its copy) as design_read reads them: layers are matched by their path of names below each root (same-named siblings in order), and the answer lists the layers only A or only B has, and for each layer both have, the attributes that differ as [A's, B's] (text as $text, an instance's variant and props too) and whether an icon's or image's own drawing differs (paths, colours, inner layers). Ids and the two roots' own names and places are left out. Works while Allow edits is off. To compare a layer with how it was when you built it, use baseline_diff.
| Parameter | Type | What it is |
|---|---|---|
a | string | The first layer's id (variant A), e.g. 54:243. |
b | string | The second layer's id (variant B). |
depthoptional | integer | Levels below each layer to compare. Default: all. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
nodes_find
Finds layers on the current page, or inside within, by name (substring), type (FRAME, TEXT, INSTANCE…), text content (substring) or component (an instance's component or component set name). Returns ids, names, types, boxes and the names of their ancestors, at most limit (more: true when there were more). Combine criteria to narrow down.
| Parameter | Type | What it is |
|---|---|---|
nameoptional | string | |
typeoptional | string | |
textoptional | string | |
componentoptional | string | |
withinoptional | string | |
limitoptional | integerdefault 50 | |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
outline
The layer tree of a frame, a page or the current page, as light XML: tag (the Figma type), id, name and box (x and y relative to the outlined node) per layer, to depth levels; an instance also names its component and variant, so repeated components show. Use it to find the sections of a big frame before reading one with design_read. A layer with $children="N" has N children not listed here: outline it on its own.
| Parameter | Type | What it is |
|---|---|---|
nodeoptional | string | The layer or page id, or 0:0 for the list of pages. Default: the current page. |
depthoptional | integerdefault 3 | How many levels below the node to list (1 to 10). |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
screenshot
Renders a layer to PNG so you can see it: a section to build, or your result to check. Returned inline at most 2000 px on a side: a larger layer comes scaled down to fit, and the text says its full size. For details of a tall layer screenshot one section of it (outline lists them), or pass clip for a region of it; pass save to write the PNG full size to a file instead.
| Parameter | Type | What it is |
|---|---|---|
node | string | The layer's id, e.g. 54:243. |
scaleoptional | numberdefault 1 | Pixels per Figma pixel: 2 for a sharp look, below 1 to fit. |
effectsoptional | booleandefault true | Include shadows and outside strokes around the layer; false renders only its own box. |
saveoptional | string | Write the PNG to this file (any size) instead of returning it; a relative path is from the server's folder. |
clipoptional | object | Only this region of the layer, in Figma px from its box's top left (before scale); with effects on it may reach into what paints around the box. |
modesoptional | object | Render in these variable modes, by collection and mode name, e.g. {"Theme": "Dark"}: set on the layer for the render, then set back (needs Allow edits; the file is left as it was). |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
selection_get
Lists the layers the user selected in Figma (id, name, type, box in page coordinates). Call it when the user says "this", "the selected one" or "what I picked", then read those ids with design_read or outline.
| Parameter | Type | What it is |
|---|---|---|
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
selection_wait
Waits until the user selects something in Figma, then answers with the selection, as selection_get does (changed: false when the time ran out first). Tell the user what to select before calling it: "select the card you mean in Figma".
| Parameter | Type | What it is |
|---|---|---|
timeoutoptional | numberdefault 60 | Seconds to wait for the user to select something: 60 by default, 300 at most. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
status
Lists the Figma files whose Pixelwright plugin is connected (name, current page, editor, whether Allow edits is on) and marks the active one: the file whose plugin window was in front last, which every tool uses when no file is named. Call it first, and whenever a tool says no file is connected: hint says what to do. While a plugin waits to be paired, pairing holds the code: tell the user to type it into the Pixelwright window, and never type or send it yourself. probe: true also pings the active file's plugin.
| Parameter | Type | What it is |
|---|---|---|
probeoptional | booleandefault false | Also ping the active file's plugin and report the round trip. |
tokens_applywrites
Writes design tokens in the shape tokens_read returns with format json: collections with their modes and variables, each mode's value in CSS (a colour, px or a number, a quoted string, true/false, var(--token) for an alias), and styles: paint (a colour or colour variable), text (the font shorthand) and effect (a box-shadow list). Collections, modes, variables and styles are found by name among the file's own or made; what the file already holds, as tokens_read writes it, is left as it is, and nothing is deleted. A library's collection or style is never copied into the file: change it in the library. Send only what you change. errors name each variable, mode or style that could not be written while the rest goes in. One undo step in Figma; refused while the plugin's Allow edits switch is off (dryRun checks without writing).
| Parameter | Type | What it is |
|---|---|---|
collectionsoptional | object[]default [] | |
stylesoptional | objectdefault {"paint":[],"text":[],"effect":[]} | |
dryRunoptional | booleandefault false | Check names, types, modes and aliases without writing; works with Allow edits off. |
versionoptional | string | A name for a version saved in the file's history before writing, for the designer to go back to (not in a dry run, nor when nothing would change). Pass one before a write that changes many layers or tokens. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
tokens_read
Reads the file's design tokens: every variable collection with all its modes (light, dark…), and the paint, text and effect styles, its own and those the current page uses from a team library, named the way design_read writes them (var(--Name)). format css (default) returns ready CSS: :root holds each collection's default mode and the styles, and every other mode is a block [data-<collection>="<mode>"] to put on the element a theme applies to; format json returns the same per collection and mode; format dtcg returns Design Tokens Format (DTCG 2025.10) trees, one per collection mode and one of styles, to save as token files for Style Dictionary or Tokens Studio. libraries: true also lists the enabled libraries' variables by name. notes say what could not be written.
| Parameter | Type | What it is |
|---|---|---|
collectionsoptional | string[] | Only these variable collections, by name (case does not matter). Default: all of them. |
librariesoptional | booleandefault false | Also list the enabled libraries' variables, by name and type: their values are read only once the file uses them. |
usedoptional | booleandefault true | Also take the styles and variables the layers on the current page (or within) use, a team library's among them. A file that uses a library has its tokens there. |
withinoptional | string | The layer or page whose used tokens to take. Default: the current page. |
formatoptional | "css" | "json" | "dtcg"default "css" | css: ready custom properties, a block per extra mode. json: the token model, per collection and mode. dtcg: Design Tokens Format (DTCG 2025.10) trees, one per collection mode (<collection>.<mode>) and one of styles, each to save as its own .json file. |
maxCharsoptional | integerdefault 80000 | Longest output; past it, only the summary comes back and you read collections one at a time. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
tokens_usage
The layers on the current page (or inside within) that use a style or variable, named by its token as design_read and tokens_read write it (--White-2 is the second style named White, --h1-font a text style), with where each uses it (fill, stroke, effect, text, a text run, or a bound field). Use it to tell same-named styles apart or to see what a change to a token would touch.
| Parameter | Type | What it is |
|---|---|---|
token | string | A style's or variable's token as design_read or tokens_read writes it: --White-2, --h1-font, --color-bg. |
withinoptional | string | The layer or page to look in. Default: the current page. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
variants_read
How each variant of a component set differs from its default one (the top-left, as Figma takes it), as nodes_diff says it: the attributes each changes as [the default's, this one's], the layers only one has, its properties, and the CSS pseudo-class a state value suggests (Hover → :hover, Pressed → :active, Focused → :focus-visible, Disabled → :disabled). Give the set, a variant or an instance. Build the default, then each state as its difference.
| Parameter | Type | What it is |
|---|---|---|
node | string | A component set's id, or one of its variants', or an instance's: its set is read. |
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
version_savewrites
Saves a named version in the file's Figma version history, for people: the designer can open the design as it was, and developers on a paid plan can pick it in Dev Mode's Compare changes. Save one only when the user asks: Figma's plugin API cannot list, read or delete a version, so it stays in the history. baselines: the baselines it goes with; it is saved only while the design still matches them, and they record it. Needs edit access to the file and the plugin's Allow edits; the same title is refused twice within ten minutes.
| Parameter | Type | What it is |
|---|---|---|
title | string | The version's name in Version history, e.g. "Built: pricing". |
descriptionoptional | string | Shown under the title. Everyone with the file sees it: no local paths or private repository names. |
baselinesoptional | string[] | The baselines this version goes with: it is saved only while the design still matches each one, and they record it. |
fileoptional | string | A session from status, or a file name. Default: the file of the first baseline, else the active file. |
viewport_get
What the user is looking at in Figma now: the page, the zoom, the part of the page in view (page px), and the page's layers and their children that meet it, outermost first. Use it when the user says "this" or "here" without selecting anything; selection_get gives what is selected.
| Parameter | Type | What it is |
|---|---|---|
fileoptional | string | The file to work on: its session from status or its name. Default: the active file. |
Guide: workflow
Which tool to use when, from finding a section to its files. The agent reads this with guide topic workflow.
Pixelwright reads the files open in Figma Desktop through its plugin; nothing goes through Figma's REST API. Every
tool takes file (a session id from status, or a file name); without it, the active file.
- Connection.
statuslists the connected files and which one is active. None connected: the user runs the Pixelwright plugin in Figma Desktop and keeps its window open (the first time, they type the pairing code status shows into the plugin window). - Find what to read.
file_map: every page with its frames, sections and components, in one call: where to start in a big file.devStatussays what the designer marked ready for dev (or completed): build those first, not drafts.selection_get: what the user selected ("this", "the selected one").selection_wait: ask the user to select something, then wait for it.viewport_get: what the user is looking at, when "this" comes with no selection.outline: the layer tree with boxes, x and y relative to the outlined layer;node: "0:0"lists the pages. A layer with$children="N"has more children than listed: outline it on its own. An instance names itscomponentandvariant, so repeated cards show without a read;dev="ready"marks what the designer handed over for development.nodes_find: layers by name, type, text or component.node_show: brings layers into view (fitted or at a zoom) and selects them, so the user sees which you mean.changes_since: what the designer changed since you last looked (a cursor), to read again only that.
- Read one section.
design_readreturns the section as XML (seeguide xml), then JSON with tokens, text styles, fonts, assets, hints and truncated, plusmeasurements(the designer's Dev Mode measurements, in px) andmotion(Figma Motion as CSS@keyframes) when the section has them. Read sections, not whole pages: pastmaxChars(80 000) the deepest branches are left empty with$truncated="true"and their ids listed intruncated; read each on its own. PassassetsDir(e.g. src/assets) to get its icons and images written as files. - Look at it.
screenshotrenders a layer to PNG (inline up to 2000 px a side, scaled down to fit;savewrites a .png file full size;cliptakes a region of it, in px from its box). Compare your build with it:image_comparerenders a layer the same way and compares it with a PNG of your build (rmse, the share of pixels that differ, the box around them, edges off by N px;diffwrites the difference as a PNG). Measure it.inspectreads layers as Figma has them, without code and with Allow edits off: the layout box and the painted box (a centred stroke, shadows and blur reach past it), the fill with the style or variables behind it (what avar()resolves to), how strokes paint, every effect's parameters (a progressive blur's start and end), a text's lines and line heights, overflow, interactions with their transitions, a component's variant.measuregives the distances between layers, each from the one before (gaps, aligned edges, insets). Compare variants.nodes_diffsays how one layer differs from another (A/B screens): the layers only one has, the attributes that differ, and icons whose drawing differs.variants_readgives a component's states as differences from its default (build :hover, :active, :disabled from them).breakpoints_findfinds a screen at the page's other widths, for the media queries.flow_readmaps the prototype: screens and the links between them. A screenshot in another variable mode (modes: {"Theme": "Dark"}) shows a theme without switching it in Figma. - The design system.
tokens_read: variables with every mode (light, dark…) and styles, as CSS, JSON or DTCG token files.tokens_usage: the layers a token is used on.components_read: the components a page uses, or the file defines, with variants and properties.design_audit: the scale a design uses (spacing, radii, font sizes, colours) and what is off it, for a file without tokens.contrast_check: every text's WCAG contrast.fonts_check: the fonts used and where the build gets each (Google Fonts links). - Files.
assets_export: several layers, or every asset inside one, as PNG, JPG, WebP, SVG or PDF, or the images as they were placed (originals);widthsizes a render by the CSS width the build shows it at (110 at scale 2: 220 px). When a file gets another name than design_read planned (planned), use the path it gives. - Write.
design_applywrites the same XML back into Figma: change layers by id, create new ones (seeguide write);tokens_applywrites variables and styles in tokens_read's JSON shape (seeguide tokens);executeruns plugin code for what neither can say. The user turns Allow edits on in the plugin window first. - Code Connect. Where the project has Figma Code Connect templates (
*.figma.ts) or batch files (*.figma.batch.json),design_readgives each connected instance as$codewith its code incodeComponents: use that code, adding whatunreadnames.components_readgives each component'scode, andmapped: falsethe ones still without: map them once with a batch row each;code_connect_readchecks the files (seeguide code-connect). - Keep the code in step with the design. Right after the
design_readyou build a screen or section from,baseline_save {node, name, code}keeps it in the project (.pixelwright/baselines/: commit it to share, or leave it out of git, since it holds the design's texts). When a session starts, or the user says the design changed,baseline_diffwithout a name says which baselines changed; withnameit lists the changes in the XML's terms. Change only the code they name: acomponentsentry whoseinstancescoverofgoes into the component's code once; export again what is markeddrawing; build added layers from theirxml(ordesign_readwhen it is left out). Thenbaseline_save {name, accept}with theacceptthe diff gave. Your owndesign_applywrites show up too: accept them once the code follows.version_saveleaves the designer a named version, only when the user asks.changes_sincestays the live view while the plugin is open.
Build the code from the XML with the project's own components, tokens and layout conventions: the XML is the spec, not the code to paste. Hints in design_read say what Figma left unbound (raw colours equal to tokens, raw spacing, radii and strokes Figma itself matches to a token, unnamed layers, frames without auto layout) and what instances changed outside their component's properties.
Guide: xml
The XML design_read returns: tags, metadata, CSS attributes, read-only attributes. The agent reads this with guide topic xml.
design_read writes a section as XML: one element per visible layer, Figma's layer type as the tag, CSS
properties as attributes.
Tags
- Figma's types in PascalCase:
Frame,Group,Section,Component,ComponentSet,Instance,Text,Rectangle,Ellipse,Line,Slot… Image: a true image (a photo, a layer filled with an image, a masked group, a layer with an effect CSS cannot draw). Itssrcis a PNG of what it shows.Icon: a vector, or a group or instance drawn only with vector shapes (a logo). Itssrcis one SVG.Span: a run inside aTextwhose style differs from the text's own.- Hidden layers and slices are left out.
Metadata attributes, in this order
id (instance children keep I…;… ids), name (left out on a text named by its content), component and
variant ("Size=Large, State=Default") on instances, props (an instance's text and boolean properties,
"Label=Sign up, Icon=true"; a comma, = or backslash inside a name or value comes escaped with a backslash),
description (components), note (Dev Mode annotations, "Category: text", several joined with " | "), href
(a prototype link, or a text's hyperlink), textStyle, src (assets).
CSS attributes
Ready CSS values, in this order: display, flex-direction, flex-wrap, position, left, right, top, bottom, width, height, min/max sizes, flex, align-self, grid-row, grid-column, justify-self, padding, justify-content, align-items, align-content, gap, column-gap, row-gap, grid-template-columns/rows, transform, opacity, overflow, text-overflow, white-space, border-radius, border (or border-top…, outline), background, box-shadow, text-shadow, filter, backdrop-filter, font, letter-spacing, text-transform, text-decoration (with text-underline-offset, text-decoration-thickness), text-align, text-wrap, color.
- Auto layout is flex (or grid); defaults are left out (row, flex-start, gap 0). A gap is written only between two or more children.
- Sizes: fixed sizes in px; hug writes nothing; fill is
flex: 1along the parent's direction andalign-self: stretchacross it. The layer you read always has its width and height. - A child of a frame without auto layout, or an absolute child, has
position: absoluteand is placed against its parent element: give that parentposition: relative. Constraints: right/bottom when pinned to the end,calc(50% ± n px)when centred, left and right without a width when stretched, percents when scaled. - A turned layer has
transform: rotate()and is placed by its centre; a mirrored onescaleX(-1). - Colours bound to a style or variable are
var(--Name)(seeguide tokens); values for them are in the JSONtokens. In a Display P3 file every colour iscolor(display-p3 r g b)(as tokens_read writes it too): keep it so, it is what the designer chose. A text with atextStylecarries only what differs from the style;textStylesholds the style's font. - Lengths are whole pixels; letter spacing keeps fractions.
Read-only attributes (start with $)
$css: what the mapping cannot write back yet, as Figma's own CSS: gradients, blend modes, line clamps, lists, a negative gap ("children overlap"), a mask among layers.$children="N": the layer has N children and not all are listed (snapshot budget or maxChars).$truncated="true": the branch was emptied, or the text shortened, to fit maxChars; its id is intruncated.$code="Button": an instance the project's Code Connect template or batch row renders, written without its children; its code is incodeComponents, whoseunreadnames the properties that code leaves out: add them (seeguide code-connect). design_apply leaves its children alone.$exposed="true": a nested instance its component exposes: its properties are the component's API too.$cssholdsproperty: valuepairs only, explained in hints:blur: progressive from 0px at 50% 0% to 8px at 50% 100%(a blur that changes across the layer;filterholds its end radius) andstroke-align: center(half of the stroke paints outside the box, which a border-box border does not;inspectgives the painted box),list-spacing: 8px(the space between a list's items, apart from paragraph spacing) andstroke-linecap: round(a line's round or square ends, which a CSS border does not draw).$override="text, background": what this layer of an instance changes that its component's properties do not say (a feature text rewritten, an icon recoloured). Code built from the component cannot show it: give the component a property or a variant for it, or build that instance as a one-off. A hint lists them all.$dev="ready for dev"on the read's root: the designer marked it (or a frame or section around it) ready for development (orcompleted).$grid-style="Desktop 12": the grid style the frame'slayout-gridcomes from.$pins="width, padding": the properties the designer pinned in the layer's Dev Mode annotations, in CSS words: take those values exactly.$motion="opacity 0→1, translate-y 24→0px; 0–0.6s ease-out": the layer's Figma Motion, led by the animation style's name when one made it. The JSON'smotionhas it as CSS: a@keyframesper animated layer and a rule for the class it names (layers), keyframes placed over the timeline; what CSS cannot animate is a comment.$on-click,$on-hover,$on-press,$on-drag,$on-mouse-enter,$on-mouse-leave,$on-mouse-down,$on-mouse-up,$on-key,$after-timeout,$on-media-hit,$on-media-end(any other trigger as$on-<trigger>): the layer's prototype interactions, e.g.$on-hover="change to Type=Hover; smart animate 300ms ease-out"(a variant to switch to on hover, and how: build it as the hover state); the easing is valid CSS, a spring drawn aslinear(). A link stayshref.
Text
is a paragraph break (Enter),
a line break inside a paragraph (Shift+Enter,<br>in HTML): never spaces.line-height: normalis Figma's Auto line height, the font's own spacing (about 1.2× the size for most fonts): give it in px where the build must match;inspectshows each text's.
Example (Orbit, a pricing card, abridged)
<Instance id="54:268" name="Plan – Pro" component="Plan Card" props="Price=$12, Popular=true, Description=For freelancers shipping fast., Name=Pro" note="The price comes from the billing API. Show the Popular badge only on the plan the API marks popular." display="flex" flex-direction="column" width="385px" height="394px" padding="var(--space-32)" gap="var(--space-24)" border-radius="var(--radius-lg)" border="2px solid var(--color-brand)" background="var(--color-surface)" box-shadow="var(--Shadow-Card)" $on-click="go to Checkout – Desktop; dissolve 300ms ease-out">
…
<Instance id="I54:268;54:211" name="Button" component="Button" variant="Variant=Primary, State=Default" props="Label=Get started" display="flex" align-self="stretch" padding="var(--space-12) var(--space-24)" justify-content="center" align-items="center" border-radius="var(--radius-md)" background="var(--color-brand)">
<Text id="I54:268;54:211;54:190" name="Label" textStyle="Button" color="var(--color-on-brand)">Get started</Text>
</Instance>
<Frame id="I54:268;54:213" name="Features" display="flex" flex-direction="column" align-self="stretch" gap="var(--space-12)" overflow="hidden">
<Frame id="I54:268;54:214" name="Feature" display="flex" align-items="center" gap="var(--space-8)" overflow="hidden">
<Icon id="I54:268;54:215" name="Icon/Check" component="Icon/Check" src="assets/icon-check.svg" width="20px" height="20px"/>
<Text id="I54:268;54:217" name="Text" textStyle="Body Small" color="var(--color-ink)">Unlimited projects</Text>
</Frame>
…
</Frame>
</Instance>
Guide: tokens
How tokens are named and written: styles, variables, modes, aliases, units, libraries. The agent reads this with guide topic tokens.
design_read writes a bound colour or length as var(--Name) and lists the values it used in tokens;
tokens_read returns the whole set.
Names
- A style: its name with every run of other characters than letters, digits, _ and - turned into "-": "Dark" →
--Dark, "Brand/Primary 500" →--Brand-Primary-500. Letters of any script stay. - A variable: its Web code syntax when the designer set one (var(--surface) →
--surface), else its name the same way ("space/4" →--space-4). - A text style is
--<name>-font, holding its font shorthand:font: var(--h3-font). - Two styles of one kind with one name (two paint styles "Black") are different tokens: the file's first keeps
--Black, the next are--Black-2,--Black-3(tokens_read notes them with their values). Use the one the layer names. The file's own styles come first; library styles are numbered among those the page uses, so every tool means one style by a name on that page (a read from another page may number a library's Black differently). Text styles are numbered alike: the second "h1" is--h1-2-fontandtextStyle="h1-2", and design_apply binds it so.
tokens_read
- It takes the file's own styles and variables, and those the current page (or
within) uses from a team library: a file built on a library has its tokens there.used: falsekeeps only the file's own. - format css (default):
:rootholds every collection's default mode and the styles; every other mode is a block[data-<collection>="<mode>"]with that collection's variables. Put the attribute on the html element,<html data-brand="dark">: a token in:rootthat points at another collection's token is computed in:rootand does not follow a mode set lower in the page. - format json: per collection, its modes and each variable's value in every mode.
- format dtcg: Design Tokens Format (DTCG 2025.10) files, one per mode of each collection, keyed
<collection>.<mode>(the collection alone when it has one mode), andstylesfor the styles; aliases are{path}, a token whose path is also a group sits at the group's$root; a token takes the type of the tokens it is aliased with (a number a gap points at is a dimension). Style Dictionary and other DTCG tools read them. Named innotes: an alias to a token no file here holds (a library's, a collection left out), a name taken twice (the second suffixed -2), and what DTCG has no type for (a text variable, a boolean, an image style). collectionskeeps only the named ones;libraries: trueadds the enabled libraries' variables by name and type.- Past
maxChars(80 000) only the summary comes back, with the collections to read one at a time. - An alias to a token the output leaves out (a collection you did not ask for) is named in
notes.
tokens_apply
- It takes what tokens_read returns with format json, or the same shape written by hand:
collections(each withname,modesandvariables:name,typeif you like,valuesby mode name,description,token) andstyles(paint,text,effect:nameandvalue). Values are CSS, as below. - Collections, modes and variables are found by name in the file's own collections, or made; a new collection's
first mode renames Figma's default one. A variable's type comes from its first value when not given.
var(--token)makes an alias, to a variable of the call (in any order) or of the file. Atokenother than the name's own becomes the variable's Web code syntax. - A paint style's value reads as a
background(colours, a colour variable), a text style's as thefontshorthand, an effect style's as abox-shadowlist. - A value a variable or style already holds is left as it is, so sending tokens_read's own output writes nothing;
send only what you change. Nothing is ever deleted (use
executeto remove a token). - A library's collection or style the file uses is never copied into the file: change it in the library.
- More modes than the file's Figma plan allows: that mode's error comes back and the other modes are written.
- One undo step;
dryRunchecks names, types, modes and aliases without writing.version: "Before the brand change"first saves a named version in the file's history, for the designer to go back to, when the call changes something.
Values
- Colours as hex (rgba when see-through); a colour with an opacity variable as
color-mix(in srgb, … N%, transparent)(in display-p3in a Display P3 file). - Numbers in px, except opacity and font weight, which have no unit.
- Strings quoted; booleans as true/false (CSS has no booleans); easings as CSS easing functions (a keyword, cubic-bezier(), or a spring drawn as linear()); durations in seconds.
- An alias is
var(--other). One that points nowhere is a comment and a note.
A text style is --<name>-font, with also holding --<name>-letter-spacing and --<name>-text-transform
when they are not the default. A gradient paint style is a token once a layer that uses it is read (Figma writes its
CSS for that layer).
Not tokens yet
Paint styles with images, gradient styles no layer read uses, blur effect styles, and library variables the file does
not use (Figma gives their values only once they are imported into the file: libraries: true lists them by name
and type). Paint styles and effect styles that are left out are named in notes; a var() design_read writes
without a value is named in its hints.
Guide: assets
What becomes a file, in which format, and how files are named. The agent reads this with guide topic assets.
Only true assets become files; everything else is CSS.
What is an asset
Icon(SVG): a vector, Figma Draw's text on a path or transform group, or a group, frame or instance without auto layout drawn only with vector shapes, at least one of them a true vector (a logo, an arrow in a circle). Exported with everything it draws, strokes outside its box included, and sized by that box.Image(PNG at 2x): a layer filled with an image, a mask group (its bottom layer is the mask), a layer with a video, pattern or shader fill or a noise, texture or glass effect. Exported as its own box: its corners, drop shadows and outline stay CSS around it, and the rest of its look (opacity, blur, inner shadows) is left to the file, as Figma renders it.- A layer the designer marked for export (PNG, JPG or SVG; a PDF export is not a web asset), in that format, when it
holds no text (an export on a section with text is a preview, read as layers). A frame with an image fill and children keeps its children: the image is its
background, written as the original image. - Plain rectangles, ellipses, buttons, cards, shadows are CSS, never images.
design_read vs assets_export
design_readwithassetsDirwrites the assets of the section it reads, and its XML points at the files.assets_exportwrites files on their own:nodes(up to 50 layers, one file each, in the format and scale you give: PNG, JPG, WebP, SVG or PDF;width, the CSS width the build shows a layer at, sets the render to width × scale px) orfrom(every asset inside one layer).original: truewrites the images as they were placed, full size and in their own format, instead of renders.- design_read without
assetsDironly plans names. When assets_export writes a file under another name (the name holds other bytes, the same bytes are another layer's file, an original is a JPEG), the file hasplanned: use itspathin thesrc.
Files
Named after the layer (Hero photo → hero-photo.png, a component's variant added: company-logo-amazon.svg). The
same bytes are written once and shared; a name already taken by other bytes gets -2; a file already in the folder is
never overwritten with other bytes.
Guide: write
design_apply: changing, creating, copying and moving layers, components, instances and images with the same XML. The agent reads this with guide topic write.
design_apply takes the XML design_read returns. The user turns Allow edits on in the plugin window first;
until then every call is refused (dryRun: true checks a call without writing, switch on or off). Before a write
that changes many layers, pass version: "Before the hero rewrite": a named version is saved in the file's history
first (not in a dry run), and the designer can go back to it; if it cannot be saved, nothing is written
(version_save saves one on its own, when the user asks). In a
Display P3 file, color(display-p3 …) (as design_read writes it) is written as it is, and hex, rgb() or hsl() are
taken as sRGB and converted to P3.
Changing a layer
- An element with
idchanges that layer, and only the attributes it gives:<Frame id="54:243" gap="48px"/>changes the gap and nothing else. Read the layer first, edit the XML, send back what you changed (sending the whole read back is fine: what the layer already has is not written again). - Leaving an attribute out never removes it. To remove, write the value that means none:
background="none",border="none",box-shadow="none",width="auto"(hug),max-width="none",flex="none",left="auto",transform="none",display="block"(no auto layout),display="none"(hide). - A text:
<Text id="…">New words</Text>replaces the characters and keeps the look; spans style runs. A Text without content keeps its text, so<Text id="…" color="var(--Dark)"/>only recolours it. A text inside an instance changes by itsI…;…id and stays an override. - What Figma keeps together is completed from the layer: a new
column-gapkeeps its direction and wrap, a newfilterkeeps its shadows, a newborder-bottomkeeps its other sides, a newwidthkeeps the edge it is pinned to. A newleftorrightreplaces both edges. - Moving: an element with
idunder another parent element moves the layer there,before="id"/after="id"puts it next to that layer, and listing layers in another order reorders them. A layer inside an instance stays where it is (only an instance's slots take new layers). - A text
design_readshortened ($truncated) is never written back: read that layer on its own to change it. - A hidden layer comes back with
display(flex,gridorblock, as it was laid out). - Each tag takes its own attributes: no
backgroundorgapon a Text (its colour iscolor), no text styles on a Frame, no auto layout on a shape; an element's id is given once (a copy has none).
Creating
- An element without
idcreates a layer:Frame,Section,Text,Rectangle,Ellipse,Line,Instance,Component,ComponentSet,Image,Icon,Slot. It goes inside its parent element, after the element before it (alone: last); at the top level intoparentor the current page, or next to a layer withbefore="id"/after="id". Groups and vectors are not drawn from XML: use anIcon. - A new layer has what its XML says and nothing more: no fill without
background, no clipping withoutoverflow; a new auto layout frame hugs unless sized; a new text is black unlesscolorsays otherwise. - A copy:
<Frame copyOf="54:268" name="Card 2">clones that layer (everything the XML cannot show comes along) and then writes the element onto the copy. Inside it, elements with the source's ids write the copy's layers: copy a card's XML, drop the root's id, addcopyOf, change the title.
Components and instances
<Instance component="Heading" variant="Property 1=Green" props="Label=Our work, Has icon=false"/>makes an instance by the names design_read and components_read write: the file's components, the ones its page uses from a library, orcomponent="key:<key>"for a library one the file does not use yet.variantpicks the variant (values left out are the set's default),propssets text and boolean properties (names without Figma's #id).- Inside a new instance, elements with another instance's ids (of the same component and variant) or the
component's own write the new instance's layers, as overrides; the other instance stays as it is.
<Instance copyOf="id">copies an instance with its overrides instead. - An existing instance:
variantswitches it,propschanges its properties,componentswaps it to another component. A comma or=inside a name or value is escaped with a backslash, as design_read writes it. <Component name="Badge">…</Component>makes a component like a frame; inside it,prop="Label"on a Text makes a text property bound to it andvisibleProp="Shown"makes a boolean property bound to the layer's visibility.<ComponentSet name="Badge">holds<Component variant="Tone=Dark">variants: they are combined into one set (the properties defined once on it) and laid out in a grid.descriptiondescribes a component or a set; an instance of a new component can follow later in the same XML.<Slot name="Content">makes a slot in a new component; in an instance, give its<Slot id="…">new layers.
Images, icons and notes
<Image src="photos/hero.jpg" width="600px" height="400px"/>makes an image from a file (PNG, JPG or GIF, up to 32 MB and 4096 px a side; convert WebP first),<Icon src="icons/arrow.svg" width="24px"/>an icon from an SVG;background: url(…)fills a frame with one. Paths are on the server's machine, from its folder.- An existing
Imagewith another file insrctakes that image, keeping its fit. A file design_read or assets_export wrote for that layer (a render of it) changes nothing wherever it went, so a read sent back keeps the originals; a picture saved over it is a new image. An existing icon is replaced by deleting it and creating a new one. note="Behavior: opens the modal | Spacing: 8px"writes the layer's Dev Mode notes, the whole list (a category the file lacks is made;note=""removes them). A note sent back as it was keeps its formatting and pinned properties; notes without text, which design_read does not show, stay.
Values
- Lengths in px;
var(--Name)binds the variable or style of that name, asdesign_readandtokens_readwrite them (a paint style, a colour or number variable, an effect style);textStyle="h3"applies a text style. fontis the shorthand[italic] weight size/line-height 'Family'; Figma must have the family and that weight, or the error lists the styles it has.- Sizes:
width/heightin px orauto;flex="1"fills an auto layout parent's direction,align-self="stretch"fills across it. Placement:position="absolute"withleft/right/top/bottom,calc(50% ± Npx)for the centre, percents (with the size in percents) to scale. - Not written yet, and refused with an error: gradients, blend modes, grow weights,
overflow: scroll, links on frames, dashed underlines.
Errors and undo
nodes_delete deletes layers by id (not those inside an instance: hide them with display="none").
Each element is written on its own: an error names its line and attribute, and the rest of the XML still goes in.
created lists the new layers' ids by line, changed the layers written to. One call is one undo step in Figma:
the user's Cmd+Z takes the whole call back.
Effects CSS cannot write (noise, texture, glass, hidden effects) stay when a layer's box-shadow or filter
changes, and a shadow sent back as it was keeps its colour variable and blend mode.
Beyond the XML: execute
execute runs JavaScript in the plugin for what design_apply and tokens_apply cannot say (removing a variable,
a prototype link, a plugin data field). The code is the body of an async function given figma (the Plugin API)
and console: await works, return gives result (Figma's layers as {id, type, name}, plain data as
built), console.log lines come back as logs, and what it throws as error. What it changes during the call is
one undo step; it needs Allow edits, and the tool waits at most 25 s. Code it starts that outlives the call (timers,
listeners, work past the timeout) keeps running until the plugin window is closed, Allow edits or not; an endless loop,
synchronous or of awaits, freezes Figma until then. Read the result back with the read tools.
Guide: code-connect
Figma Code Connect: templates and batch files, keys, how design_read runs them, what the code leaves out, mapping a design system. The agent reads this with guide topic code-connect.
Pixelwright runs the project's Figma Code Connect files itself, on any Figma plan, without a token and without
publishing anything: design_read gives each instance a connection builds as $code in the XML (no children) and
its code in codeComponents ({ ids, component, template, source, snippet, imports, unread }: instances with the
same code once), with the read's imports once each and the failures in codeErrors ({ template, error, ids }).
Nested connected instances are inside their parent's snippet; the layer the read is of is never collapsed. The code
takes at most 40% of maxChars (the XML the rest), and all templates of a read five seconds: past either, instances
are read as layers. code: false reads every instance as layers.
Mapping a design system
design_readthe section; its hints name the components shown without code.components_read within=<id> mapped=falsegives each one'skey, properties, variants andexposedinstances.- Find the code component that builds it in the project (search the code;
links: truereads the links people attached in Dev Mode, on paid plans, with the project file a code link points at). - Confirm each pair with the user.
- Write a batch row (below), or a template for a component that needs logic of its own.
code_connect_readchecks it:by,unread,values,missingand the problems.design_readagain: the instances are$codenow.
Batch files: one row per component
A *.figma.batch.json file (Figma's own format) holds one group { "templateFile", "components" }, or an array
of them; each row runs through the group's shared template (ending .figma.batch.ts or .js, next to the batch
file), which reads the row as figma.batch:
{
"templateFile": "./component.figma.batch.ts",
"components": [
{ "key": "2f31fdc95b9804804df14c93a7f9c8913d611c17", "component": "PricingTier",
"source": "src/components/PricingTier.tsx",
"imports": ["import { PricingTier } from \"@/components/PricingTier\""],
"props": { "Name": "name", "Price": "price", "Popular": "popular",
"Tier": { "prop": "tier", "values": { "Pro": "pro", "Free": "free" } },
"Description": "children" } }
]
}
The shared template (examples/orbit/components/component.figma.batch.ts) writes the row's component with
each props entry: "Name": "name" passes the value, { "prop", "values" } maps a variant's (getEnum) or a
toggle's (getBoolean) value, a prop named children goes between the tags, true is a bare prop, false,
null and "" write nothing; imports are the row's, the id its id else the component. For Vue, use
the same template with figma.code in place of figma.tsx: a Vue template takes the same tags, string props and
bare toggles. A row is named <batch file>#<n>, n counting from 1 across the file's groups.
Keys and matching
components_read gives each component's key, the same in every file, a library's included. A row's "key" or
a template's // key= (Pixelwright's addition, which Figma's CLI skips) connects exactly that component or set:
a keyed connection matches by key only. Figma's CLI needs a url (// url=) to publish a file; Pixelwright does
not, and says so once per file. Without a key, a connection matches by the id its url names (a component this file
defines) with the names agreeing, then by name (the Figma name's last part against component, else the file name
before .figma., case, spaces and punctuation aside: Buttons/Primary Button matches PrimaryButton), then by
the id alone. code_connect_read gives the rule each matched by as by (key, id, name). A component made
again, or copied, gets a new key: its keyed connection then misses, and the component shows as unconnected.
What the code leaves out
unreadnames the properties no code read: the instance's own (Description), and those of the nested instances it exposes or whose properties it overrides (Button.Label). A text whose value the snippet holds counts as read. Write them in, or read the instance withnode=<id>: the values are on the element (variant,props).values(code_connect_read) names the variant values agetEnummapping misses: Figma renders them asundefined.missingmarks asourcethat names a file in neither the project nor beside the Code Connect file (packages and URLs are not checked).exposed(components_read) and$exposed(design_read) mark the nested instances a component exposes: their properties are the component's API too, so in code they are its props.
Where the files are
The project folder is CLAUDE_PROJECT_DIR, else the server's working directory (never the home folder or the disk's
root). Code Connect files are those ending in .figma.ts, .figma.js, .figma.template.ts,
.figma.template.js or .figma.batch.json among those figma.config.json's codeConnect.include globs name
(minus exclude; its documentUrlSubstitutions are applied to urls), else all such files; dependency and build
folders are never searched. A batch file a custom include misses is left out, as Figma does, and named in the
problems.
Writing one
Write it as Figma documents it, with types from the npm package @figma/code-connect (in tsconfig:
"types": ["@figma/code-connect/figma-types"]):
// url=https://www.figma.com/design/<fileKey>/Kit?node-id=1-2
// component=Button
// source=src/components/Button.tsx
import figma from 'figma'
const button = figma.selectedInstance
const size = button.getEnum('Size', { Small: 'sm', Large: 'lg' })
const icon = button.getInstanceSwap('Icon')
export default {
example: figma.tsx`<Button size="${size}"${figma.helpers.react.renderProp('disabled', button.getBoolean('Disabled'))}>${button.getString('Label')}${icon?.executeTemplate().example}</Button>`,
imports: ['import { Button } from "@/components/Button"'],
id: 'button',
}
url: the component's or component set's link (Copy link in Figma), orkey(above).component: the code component's name, as Figma's Code Connect defines it ($codeshows it).source: the code component's file.- Imports:
figmaand the template's own relative helpers (import { sizes } from './helpers', TypeScript or JavaScript); nothing else. - The API:
figma.selectedInstance(aliascurrentLayer) withgetString,getBoolean(name, mapping?),getEnum,getPropertyValue,getInstanceSwap,getSlot(the slot layer named after the property,connectedInstances),findInstance,findText,findLayers,findConnectedInstance(s)(withpathandtraverseInstances),executeTemplate,hasCodeConnect,codeConnectId,id,properties;figma.code,tsx,html,swift,kotlin,figma.helpers.react.*,figma.properties.*. Property names are as Figma shows them (without the #id suffix). Hidden layers are not found. - Errors are the instance's alone:
PROPERTY_NOT_FOUND,CHILD_LAYER_NOT_FOUND(when the missing layer is rendered),TEMPLATE_TIMEOUT(one second per instance, nested templates included; the template is not run again in that read),TEMPLATE_BUDGET,TEMPLATE_IMPORT,TEMPLATE_CYCLE,TEMPLATE_CUT(the read left out layers the code needs: read the instance on its own). Such an instance is read as usual, with its children. - Old
figma.connect(...)files are not run:npx figma connect migrateturns them into templates.
Templates run on this computer as the project's own code, in a separate context without require, process or
files, but that context is no protection against a hostile template: run Pixelwright only in projects you trust, as
you would their build scripts.
Guide: email
Turning the XML into an HTML email that renders in Gmail, Outlook and Apple Mail. The agent reads this with guide topic email.
Email clients are not browsers: Gmail strips <style> blocks it does not like, Outlook on Windows renders with Word
and ignores flex, grid, position and most of CSS. Read the section with design_read (and screenshot to compare),
then translate:
Layout
- The design's width (usually 600 px) becomes a centred
<table width="600">inside a 100%-wide wrapper table. - Every flex column is a stack of table rows; every flex row is one row with a
<td>per child;gapbecomes padding on the cells or spacer cells;justify-content/align-itemsbecomealignandvalignon cells. position: absolutehas no email equivalent: text over a picture means a background image on the cell (with a solid background colour as the fallback) or one image with the text in it andalt.- Write sizes as attributes too (
width,heighton tables, cells and images), not only in CSS.
Styles
- Inline every style on its element (
style="…"); keep a small<style>only for media queries and dark mode. - Colours from
tokensas plain hex: email clients do not supportvar(). - Fonts: the
fontslist tells which families the design uses. Web fonts load in Apple Mail and some others only: always end the stack with a safe fallback ('Satoshi', Arial, sans-serif). - Borders, radius, padding and background colours work on cells;
box-shadow,filterandbackdrop-filterdo not render in most clients: drop them or bake them into an image.
Images
- Export with
assetsDirorassets_exportat 2x (the default) and give each<img>its 1xwidthandheightattributes,display:block,border:0and a realalt. - Icons as SVG do not render in many clients: export them as PNG (
assets_exportwithformat: "PNG"). - Host the files on a server or CDN; email clients cannot read local paths.
Links and text
- Every button is a link (
hrefin the XML when the design has one): a table cell with the background colour and an<a>with padding inside, never a<button>. - Keep the text as text, not images, so it can be read and translated; give line heights in px.
Dark mode
Clients invert colours on their own; check the result with a screenshot of the light design, and add
@media (prefers-color-scheme: dark) rules (supported in Apple Mail and Outlook for Mac) for logos and backgrounds
that turn unreadable.