Capture a Figma node link to a PNG by driving an already-logged-in Chrome over
the Chrome DevTools Protocol (CDP). No Figma API token, no OAuth — it reuses your
existing Chrome session, the same way the sibling chatgpt-cli drives ChatGPT.
figma-cli capture "https://www.figma.com/design/<key>/<slug>?node-id=457-37833" -o node.png
Figma renders the canvas in WebGL, so there is no DOM element per design
object to screenshot. figma-cli instead:
- Attaches to your running Chrome at
127.0.0.1:<port>(CDP). - Opens the node URL — the
node-iddeep link makes Figma fit-and-center that node (this is the framing mechanism; keyboard zoom shortcuts proved unreliable and are not used). - Hides Figma's UI chrome via the DOM (keeps only the
<canvas>visible). - Sets a deterministic high-DPI viewport and waits for the WebGL surface to finish painting (render stabilization — a fixed delay races the async paint).
- Captures the viewport with
Page.captureScreenshotand writes the PNG.
Requires the Go 1.26 toolchain (auto-fetched by Go's toolchain mechanism on a 1.24+ install; chromedp needs ≥1.25).
go build -o figma-cli ./cmd/figma-cli
-
Set a Chrome profile directory so the CLI can launch a dedicated, logged-in Chrome. Either in
~/.config/figma-cli.yaml:profilePath: /Users/you/figma-chrome-profile
…or via the environment (shared with
chatgpt-cli):export BROWSER_PROFILE_PATH=/Users/you/figma-chrome-profile -
Launch Chrome with remote debugging and log into Figma in it:
figma-cli launchKeep that Chrome window open. (
launchis macOS-only; on other platforms it prints theopenargs to run manually.) -
Confirm connectivity:
figma-cli status --pretty
Capture a node to a PNG.
| Flag | Default | Description |
|---|---|---|
-o, --output |
<fileKey>_<nodeId>.png |
Output path (parent dirs created) |
--scale |
2 |
Device scale factor (higher = sharper/larger) |
--width / --height |
1920 / 1080 |
Viewport size in CSS pixels |
--settle-ms |
1500 |
Render-stabilization settle window |
--reuse |
off | Reuse an existing Figma tab instead of opening one |
--no-hide-ui |
off | Keep Figma's UI chrome in the shot |
--force |
off | Overwrite an existing output file |
--allow-no-node |
off | Allow a URL without a node-id (whole file) |
--tile <maxpx> |
off | Render large and slice into tiles ≤ maxpx/edge (see below) |
# Basic
figma-cli capture "<url>" -o node.png
# Sharper
figma-cli capture "<url>" -o node.png --scale 4 --force
# Reuse your open tab, human-readable output
figma-cli capture "<url>" -o node.png --reuse --pretty
A big node (e.g. a tall board) squeezed into one viewport is unreadable. --tile
renders the node at high resolution (auto-sized to its aspect ratio), trims the
canvas background, and slices it into a grid of PNG tiles:
figma-cli capture "<url>" -o out/node.png --tile 2000
# writes out/node-r0-c0.png, out/node-r0-c1.png, ... (each <= 2000px/edge)
The JSON result lists every tile with its position and the grid (rows,
cols). Blank tiles (empty canvas around the node) are dropped. Auto-resolution
is capped at a 10000px long edge to stay within GPU limits. Note: a tiled
capture does two renders (probe + final), so it takes longer (~40s); aspect
auto-detection is heuristic and may leave a few sparse tiles on nodes with large
empty areas.
Source map (reassembly). Alongside the tiles, --tile writes
<base>-tiles.json — a manifest that records the stitched canvas size and every
tile's pixel rect (x, y, width, height) and grid position, so the tiles
can be mapped back into one image:
{
"canvasWidth": 7587, "canvasHeight": 10000, "rows": 5, "cols": 4,
"tiles": [
{"file": "node-r0-c0.png", "row": 0, "col": 0, "x": 0, "y": 0, "width": 1896, "height": 2000},
{"file": "node-r0-c1.png", "row": 0, "col": 1, "x": 1896, "y": 0, "width": 1897, "height": 2000}
]
}Reassemble with any compositor, e.g. ImageMagick:
magick -size 7587x10000 xc:none \
\( node-r0-c0.png -geometry +0+0 \) -composite \
\( node-r0-c1.png -geometry +1896+0 \) -composite \
stitched.png
# (or script the loop from the manifest's x/y)Spawn Chrome detached with the configured profile and remote-debugging port,
opening Figma. Alias: open.
Report the resolved config, whether Chrome CDP is reachable, and how many Figma tabs are open.
JSON envelope on stdout by default; --pretty prints a human line instead.
Errors go to stderr as {"ok":false,"error":{"message","phase"}}.
Exit codes: 0 success, 1 runtime/browser/CDP error, 2 usage/config error.
{
"ok": true,
"data": {
"path": "node.png",
"fileKey": "vXA4JtfQ0xMmsPuwT4w2Ls",
"nodeId": "457:37833",
"width": 3840, "height": 2160, "bytes": 292369, "scale": 2
}
}--pretty, --verbose (step logs to stderr), --config <path>,
--port <n> (override remote-debugging port), --timeout <ms>.
| YAML key | Env var | Default |
|---|---|---|
profilePath |
BROWSER_PROFILE_PATH |
(required for launch) |
remoteDebuggingPort |
FIGMA_BROWSER_REMOTE_DEBUGGING_PORT |
9222 |
figmaUrl |
FIGMA_BROWSER_URL |
https://www.figma.com/ |
timeoutMs |
FIGMA_BROWSER_TIMEOUT_MS |
60000 |
chromeApp |
FIGMA_BROWSER_CHROME_APP |
Google Chrome |
deviceScaleFactor |
FIGMA_CAPTURE_SCALE |
2 |
viewportWidth |
FIGMA_CAPTURE_WIDTH |
1920 |
viewportHeight |
FIGMA_CAPTURE_HEIGHT |
1080 |
settleMs |
FIGMA_CAPTURE_SETTLE_MS |
1500 |
Env overrides YAML; both override defaults.
- Framing is fit-to-node, not a tight crop. The node is centered with some
canvas padding; output fidelity is screen resolution (raised via
--scale). - No Figma API. Pixel-perfect official exports would require a token; this tool deliberately uses only the browser session.
- macOS-first automated launch.
go build ./...
go test ./...
go vet ./...
Browser paths are verified manually against a live Chrome:
FIGMA_LIVE=1 go test ./figma/ -run Live -v.