---
name: figma-plugin-builder
description: "Build Figma plugins — manifest, UI, plugin API, network access, and publish-to-Community pipeline. Scaffolds and ships Figma plugins: manifest.json setup, the plugin/UI message bridge, Figma Plugin API patterns (traverse nodes, create/modify, read variables), network access, TypeScript + bundler config, and the Community-publishing checklist. Covers both plugins and the newer widget API."
license: MIT
---

# Figma Plugin Builder

A scaffolding and reference skill for building Figma plugins and widgets end to end — from `manifest.json` through the sandbox/UI message bridge, the Plugin API, network access, and the Community submission checklist. It's aimed at developers who know TypeScript but don't want to relearn Figma's split execution model and manifest quirks every time they start a plugin.

## When to use it

- Starting a new plugin from zero and wanting a correct manifest, build config, and `code.ts`/`ui.html` split on the first try.
- Wiring two-way communication between the plugin sandbox and an iframe UI.
- Reading or writing design tokens via the Variables API, or batch-modifying nodes by traversing the document tree.
- Calling an external API from a plugin and hitting CORS or `allowedDomains` errors.
- Preparing a build for Community publishing and unsure what the review checklist wants.

## What it does

The skill generates a `manifest.json` with the fields Figma actually validates (`api`, `main`, `ui`, `editorType`, `networkAccess`, `capabilities`), and sets up the two-context architecture: privileged sandbox code (`code.ts`, runs in Figma's QuickJS, has `figma.*`) and the UI iframe (`ui.html`, runs in a browser context, no Plugin API). It scaffolds the bridge with `figma.ui.postMessage(...)` / `figma.ui.onmessage` on one side and `parent.postMessage({ pluginMessage }, '*')` / `window.onmessage` on the other.

For the API itself it covers node traversal (`figma.currentPage.selection`, `findAll`, `findChildren`), creation (`figma.createRectangle`, `createText` with `figma.loadFontAsync` before setting `characters`), and the Variables API (`figma.variables.getLocalVariableCollectionsAsync`, `getVariableById`, `setBoundVariable`). It also handles `figma.clientStorage` for persistence, the `figma.widget` API (`useSyncedState`, `usePropertyMenu`, `AutoLayout`) for widgets, and `networkAccess.allowedDomains` for fetch.

It ships a TypeScript + bundler setup (esbuild or webpack with `@figma/plugin-typings`) that inlines the UI into a single HTML file, which `manifest.ui` requires.

## Usage example

```bash
curl -fsSL --create-dirs -o ~/.claude/skills/figma-plugin-builder/SKILL.md https://claudeskil.com/skills/figma-plugin-builder/SKILL.md
# then, in your project:
"Scaffold a Figma plugin that recolors selected frames using a variable collection"
```

Minimal manifest the skill produces:

```json
{
  "name": "Recolor",
  "id": "your-plugin-id",
  "api": "1.0.0",
  "main": "dist/code.js",
  "ui": "dist/ui.html",
  "editorType": ["figma"],
  "networkAccess": { "allowedDomains": ["none"] }
}
```

## Tips & gotchas

- `figma.ui` only exists if you called `figma.showUI(__html__)` — referencing it otherwise throws.
- Always `await figma.loadFontAsync(node.fontName)` before mutating `characters`, or you'll get a font-not-loaded error.
- `manifest.ui` points to one self-contained HTML file; your bundler must inline JS and CSS, not emit separate assets.
- `networkAccess` is mandatory in the manifest now — use `{ "allowedDomains": ["none"] }` explicitly if you make no requests, or review rejects it.

## FAQ

**Can the same plugin run in FigJam and Dev Mode?** Yes — list each surface in `editorType` (`"figma"`, `"figjam"`, `"dev"`). Dev Mode plugins are read-only.

**Why does `document.getElementById` fail in `code.ts`?** The sandbox has no DOM. Do all DOM work in the UI iframe and pass data across via `postMessage`.

**Do I need review for a private plugin?** No. Org/private plugins skip Community review; only public submissions go through it.
