Pixelwright GitHub

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:

  1. 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.)
  2. Open your file and run Plugins → Development → Pixelwright. Keep its small window open while you work.
  3. The first time, the window asks for a pairing code: your agent shows it (from status); type it in.
  4. 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_apply and tokens_apply take dryRun to 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.
  • execute runs 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.

ParameterTypeWhat it is
nodesoptionalstring[]Layers to write, one file each: SVG for an icon, PNG otherwise, unless format says. Give nodes or from.
fromoptionalstringA layer to write every true asset of (its icons and images, as design_read plans them). Give nodes or from.
dirstringThe 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.
scaleoptionalnumberFor PNG, JPG and WEBP: pixels per Figma pixel, or per CSS px with width. Default 2.
widthoptionalnumberFor 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).
qualityoptionalintegerFor WEBP: 1 to 100. Default 85.
originaloptionalboolean
default false
Write the images as they were placed (not cropped, filtered or scaled) instead of renders, with their real format.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nameoptionalstringOne baseline in detail. Without it: every baseline in the project, with how much changed.
nodeoptionalstringCompare with this layer instead of the baseline's own, e.g. a v2 the designer made beside it. Needs name.
fileoptionalstringWith 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer the code is built from: a frame, section, component or instance. Default: the one selected layer. Not with accept.
nameoptionalstringNamed after the code: pricing, plan-card. Default: the layer's name as a slug. Required with accept.
codeoptionalstring[]The project's files built from the layer, relative to the project. Default: the list the baseline already has.
acceptoptionalstringThe accept a baseline_diff answer gave: keeps exactly the design that diff compared.
replaceoptionalboolean
default false
Start the named baseline over from the layer as it is now. Not with accept.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodestringA screen's id, or a layer's on it.
fileoptionalstringThe 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.

ParameterTypeWhat it is
cursoroptionalintegerThe cursor an earlier answer gave: only what changed after it. Default: all the plugin has seen.
fileoptionalstringThe 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).

ParameterTypeWhat it is
nodeoptionalstringA layer or page id to look in, e.g. 54:243. Default: the current page.
fileoptionalstringThe 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.

ParameterTypeWhat it is
scopeoptional"local" | "used"
default "used"
used: what the page (or within) uses; local: what the file defines.
withinoptionalstringA layer or page id to look in. Default: the current page (used) or every page (local).
queryoptionalstringOnly components whose name contains this.
limitoptionalinteger
default 100
mappedoptionalbooleantrue: only components a Code Connect file connects; false: only those none connects. Default: all.
linksoptionalboolean
default false
Also read the links people attached in Dev Mode (Figma's servers, paid plans only).
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer whose texts to check (a section, a screen). Default: the one layer selected in Figma.
fileoptionalstringThe 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.

ParameterTypeWhat it is
xmlstringPixelwright XML, as design_read writes it: an element with id changes that layer (only the attributes it gives), one without id creates a layer.
parentoptionalstringWhere top-level new layers go: a frame's or page's id. Default: the current page, beside its layers.
dryRunoptionalboolean
default false
Check ids, tokens and fonts without writing; works with Allow edits off.
versionoptionalstringA 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.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer to audit (a screen, a section). Default: the one layer selected in Figma.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer's id, e.g. 54:243. Default: the one layer selected in Figma.
depthoptionalintegerLevels below the layer to read. Default: all.
assetsDiroptionalstringFolder 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.
screenshotoptionalboolean
default false
Also return the layer's image, to compare your build with.
maxCharsoptionalinteger
default 80000
Longest XML to return; deeper branches past it come back empty with $truncated, to read on their own.
codeoptionalboolean
default true
Run the project's Code Connect templates, giving connected instances as $code with their code (default); false reads them as layers.
fileoptionalstringThe 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.

ParameterTypeWhat it is
codestringJavaScript run as the body of an async function with figma (the Plugin API) and console: await works, return gives the result.
timeoutoptionalnumber
default 10
Seconds to wait for the code: 10 by default, 25 at most.
fileoptionalstringThe 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).

ParameterTypeWhat it is
fileoptionalstringThe 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-*.

ParameterTypeWhat it is
pageoptionalstringThe page's id. Default: the current page.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer whose texts' fonts to check (a section, a screen). Default: the one layer selected in Figma.
fileoptionalstringThe 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.

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

ParameterTypeWhat it is
nodestringThe layer's id, e.g. 54:243.
referencestringThe 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.
scaleoptionalnumber
default 1
Pixels per Figma pixel of the render: the reference's density.
effectsoptionalboolean
default true
Render shadows and outside strokes around the layer; false renders only its own box.
clipoptionalobjectOnly 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.
modesoptionalobjectRender 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).
diffoptionalstringA .png path to write the difference to: the render faded, every differing pixel red.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodesoptionalstring[]The layers' ids, e.g. ["54:243"]. Default: the layers selected in Figma.
relativeTooptionalstringA layer (a section, a frame) whose top left the boxes are measured from. Default: the page's.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodesoptionalstring[]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.
paintedoptionalboolean
default false
Measure what is painted (centred or outside strokes, shadows, blur) instead of the layout boxes.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer's id, e.g. 54:243. Give node or nodes.
nodesoptionalstring[]Several layers on one page, fitted in view together.
zoomoptionalstring | number
default "fit"
"fit" (default), or a zoom: 1 is 100%, 0.5 is 50%.
selectoptionalboolean
default true
Select them too (default), or only bring them into view.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodesstring[]The layers' ids, e.g. ["54:243"].
fileoptionalstringThe 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.

ParameterTypeWhat it is
astringThe first layer's id (variant A), e.g. 54:243.
bstringThe second layer's id (variant B).
depthoptionalintegerLevels below each layer to compare. Default: all.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nameoptionalstring
typeoptionalstring
textoptionalstring
componentoptionalstring
withinoptionalstring
limitoptionalinteger
default 50
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodeoptionalstringThe layer or page id, or 0:0 for the list of pages. Default: the current page.
depthoptionalinteger
default 3
How many levels below the node to list (1 to 10).
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodestringThe layer's id, e.g. 54:243.
scaleoptionalnumber
default 1
Pixels per Figma pixel: 2 for a sharp look, below 1 to fit.
effectsoptionalboolean
default true
Include shadows and outside strokes around the layer; false renders only its own box.
saveoptionalstringWrite the PNG to this file (any size) instead of returning it; a relative path is from the server's folder.
clipoptionalobjectOnly 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.
modesoptionalobjectRender 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).
fileoptionalstringThe 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.

ParameterTypeWhat it is
fileoptionalstringThe 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".

ParameterTypeWhat it is
timeoutoptionalnumber
default 60
Seconds to wait for the user to select something: 60 by default, 300 at most.
fileoptionalstringThe 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.

ParameterTypeWhat it is
probeoptionalboolean
default 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).

ParameterTypeWhat it is
collectionsoptionalobject[]
default []
stylesoptionalobject
default {"paint":[],"text":[],"effect":[]}
dryRunoptionalboolean
default false
Check names, types, modes and aliases without writing; works with Allow edits off.
versionoptionalstringA 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.
fileoptionalstringThe 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.

ParameterTypeWhat it is
collectionsoptionalstring[]Only these variable collections, by name (case does not matter). Default: all of them.
librariesoptionalboolean
default false
Also list the enabled libraries' variables, by name and type: their values are read only once the file uses them.
usedoptionalboolean
default 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.
withinoptionalstringThe 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.
maxCharsoptionalinteger
default 80000
Longest output; past it, only the summary comes back and you read collections one at a time.
fileoptionalstringThe 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.

ParameterTypeWhat it is
tokenstringA style's or variable's token as design_read or tokens_read writes it: --White-2, --h1-font, --color-bg.
withinoptionalstringThe layer or page to look in. Default: the current page.
fileoptionalstringThe 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.

ParameterTypeWhat it is
nodestringA component set's id, or one of its variants', or an instance's: its set is read.
fileoptionalstringThe 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.

ParameterTypeWhat it is
titlestringThe version's name in Version history, e.g. "Built: pricing".
descriptionoptionalstringShown under the title. Everyone with the file sees it: no local paths or private repository names.
baselinesoptionalstring[]The baselines this version goes with: it is saved only while the design still matches each one, and they record it.
fileoptionalstringA 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.

ParameterTypeWhat it is
fileoptionalstringThe 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.

  1. Connection. status lists 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).
  2. Find what to read.
    • file_map: every page with its frames, sections and components, in one call: where to start in a big file. devStatus says 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 its component and variant, 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.
  3. Read one section. design_read returns the section as XML (see guide xml), then JSON with tokens, text styles, fonts, assets, hints and truncated, plus measurements (the designer's Dev Mode measurements, in px) and motion (Figma Motion as CSS @keyframes) when the section has them. Read sections, not whole pages: past maxChars (80 000) the deepest branches are left empty with $truncated="true" and their ids listed in truncated; read each on its own. Pass assetsDir (e.g. src/assets) to get its icons and images written as files.
  4. Look at it. screenshot renders a layer to PNG (inline up to 2000 px a side, scaled down to fit; save writes a .png file full size; clip takes a region of it, in px from its box). Compare your build with it: image_compare renders 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; diff writes the difference as a PNG). Measure it. inspect reads 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 a var() 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. measure gives the distances between layers, each from the one before (gaps, aligned edges, insets). Compare variants. nodes_diff says how one layer differs from another (A/B screens): the layers only one has, the attributes that differ, and icons whose drawing differs. variants_read gives a component's states as differences from its default (build :hover, :active, :disabled from them). breakpoints_find finds a screen at the page's other widths, for the media queries. flow_read maps 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.
  5. 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).
  6. 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); width sizes 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.
  7. Write. design_apply writes the same XML back into Figma: change layers by id, create new ones (see guide write); tokens_apply writes variables and styles in tokens_read's JSON shape (see guide tokens); execute runs plugin code for what neither can say. The user turns Allow edits on in the plugin window first.
  8. Code Connect. Where the project has Figma Code Connect templates (*.figma.ts) or batch files (*.figma.batch.json), design_read gives each connected instance as $code with its code in codeComponents: use that code, adding what unread names. components_read gives each component's code, and mapped: false the ones still without: map them once with a batch row each; code_connect_read checks the files (see guide code-connect).
  9. Keep the code in step with the design. Right after the design_read you 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_diff without a name says which baselines changed; with name it lists the changes in the XML's terms. Change only the code they name: a components entry whose instances cover of goes into the component's code once; export again what is marked drawing; build added layers from their xml (or design_read when it is left out). Then baseline_save {name, accept} with the accept the diff gave. Your own design_apply writes show up too: accept them once the code follows. version_save leaves the designer a named version, only when the user asks. changes_since stays 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). Its src is a PNG of what it shows.
  • Icon: a vector, or a group or instance drawn only with vector shapes (a logo). Its src is one SVG.
  • Span: a run inside a Text whose 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: 1 along the parent's direction and align-self: stretch across 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: absolute and is placed against its parent element: give that parent position: 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 one scaleX(-1).
  • Colours bound to a style or variable are var(--Name) (see guide tokens); values for them are in the JSON tokens. In a Display P3 file every colour is color(display-p3 r g b) (as tokens_read writes it too): keep it so, it is what the designer chose. A text with a textStyle carries only what differs from the style; textStyles holds 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 in truncated.
  • $code="Button": an instance the project's Code Connect template or batch row renders, written without its children; its code is in codeComponents, whose unread names the properties that code leaves out: add them (see guide code-connect). design_apply leaves its children alone.
  • $exposed="true": a nested instance its component exposes: its properties are the component's API too.
  • $css holds property: value pairs only, explained in hints: blur: progressive from 0px at 50% 0% to 8px at 50% 100% (a blur that changes across the layer; filter holds its end radius) and stroke-align: center (half of the stroke paints outside the box, which a border-box border does not; inspect gives the painted box), list-spacing: 8px (the space between a list's items, apart from paragraph spacing) and stroke-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 (or completed).
  • $grid-style="Desktop 12": the grid style the frame's layout-grid comes 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's motion has it as CSS: a @keyframes per 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 as linear(). A link stays href.

Text

  • &#10; is a paragraph break (Enter), &#8232; a line break inside a paragraph (Shift+Enter, <br> in HTML): never spaces.
  • line-height: normal is 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; inspect shows 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-font and textStyle="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: false keeps only the file's own.
  • format css (default): :root holds 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 :root that points at another collection's token is computed in :root and 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), and styles for 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 in notes: 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).
  • collections keeps only the named ones; libraries: true adds 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 with name, modes and variables: name, type if you like, values by mode name, description, token) and styles (paint, text, effect: name and value). 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. A token other 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 the font shorthand, an effect style's as a box-shadow list.
  • 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 execute to 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; dryRun checks 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-p3 in 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_read with assetsDir writes the assets of the section it reads, and its XML points at the files.
  • assets_export writes 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) or from (every asset inside one layer). original: true writes the images as they were placed, full size and in their own format, instead of renders.
  • design_read without assetsDir only 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 has planned: use its path in the src.

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 id changes 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 its I…;… id and stays an override.
  • What Figma keeps together is completed from the layer: a new column-gap keeps its direction and wrap, a new filter keeps its shadows, a new border-bottom keeps its other sides, a new width keeps the edge it is pinned to. A new left or right replaces both edges.
  • Moving: an element with id under 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_read shortened ($truncated) is never written back: read that layer on its own to change it.
  • A hidden layer comes back with display (flex, grid or block, as it was laid out).
  • Each tag takes its own attributes: no background or gap on a Text (its colour is color), 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 id creates 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 into parent or the current page, or next to a layer with before="id" / after="id". Groups and vectors are not drawn from XML: use an Icon.
  • A new layer has what its XML says and nothing more: no fill without background, no clipping without overflow; a new auto layout frame hugs unless sized; a new text is black unless color says 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, add copyOf, 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, or component="key:<key>" for a library one the file does not use yet. variant picks the variant (values left out are the set's default), props sets 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: variant switches it, props changes its properties, component swaps 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 and visibleProp="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. description describes 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 Image with another file in src takes 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, as design_read and tokens_read write them (a paint style, a colour or number variable, an effect style); textStyle="h3" applies a text style.
  • font is 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/height in px or auto; flex="1" fills an auto layout parent's direction, align-self="stretch" fills across it. Placement: position="absolute" with left/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

  1. design_read the section; its hints name the components shown without code.
  2. components_read within=<id> mapped=false gives each one's key, properties, variants and exposed instances.
  3. Find the code component that builds it in the project (search the code; links: true reads the links people attached in Dev Mode, on paid plans, with the project file a code link points at).
  4. Confirm each pair with the user.
  5. Write a batch row (below), or a template for a component that needs logic of its own.
  6. code_connect_read checks it: by, unread, values, missing and the problems.
  7. design_read again: the instances are $code now.

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

  • unread names 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 with node=<id>: the values are on the element (variant, props).
  • values (code_connect_read) names the variant values a getEnum mapping misses: Figma renders them as undefined.
  • missing marks a source that 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), or key (above). component: the code component's name, as Figma's Code Connect defines it ($code shows it). source: the code component's file.
  • Imports: figma and the template's own relative helpers (import { sizes } from './helpers', TypeScript or JavaScript); nothing else.
  • The API: figma.selectedInstance (alias currentLayer) with getString, getBoolean(name, mapping?), getEnum, getPropertyValue, getInstanceSwap, getSlot (the slot layer named after the property, connectedInstances), findInstance, findText, findLayers, findConnectedInstance(s) (with path and traverseInstances), 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 migrate turns 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; gap becomes padding on the cells or spacer cells; justify-content/align-items become align and valign on cells.
  • position: absolute has 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 and alt.
  • Write sizes as attributes too (width, height on 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 tokens as plain hex: email clients do not support var().
  • Fonts: the fonts list 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, filter and backdrop-filter do not render in most clients: drop them or bake them into an image.

Images

  • Export with assetsDir or assets_export at 2x (the default) and give each <img> its 1x width and height attributes, display:block, border:0 and a real alt.
  • Icons as SVG do not render in many clients: export them as PNG (assets_export with format: "PNG").
  • Host the files on a server or CDN; email clients cannot read local paths.

Links and text

  • Every button is a link (href in 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.