@tank/figma-plugin
1.2.0Description
Guidance for building Figma plugins. Covers architecture, Plugin API (nodes, styles, variables, components), UI development, Dev Mode codegen, widgets, publishing, and monetization.
Triggered by
tank install @tank/figma-pluginFigma Plugin Development
Expert guidance for building Figma plugins β from architecture and API usage to UI development, publishing, and monetization.
Synthesized from: Official Figma Plugin API Documentation, @figma/plugin-typings, figma/plugin-samples, Tokens Studio, FigmaToCode, Design Lint, Config talks, Evil Martians guides.
When to Use This Skill
Use when the task involves:
- Creating a new Figma plugin (design mode, FigJam, Dev Mode, Slides)
- Working with the Figma Plugin API (nodes, styles, variables, components)
- Building plugin UI (iframe, React, message passing)
- Generating code from Figma designs (codegen plugins)
- Publishing or monetizing a Figma plugin
- Debugging plugin issues (sandbox, font loading, network, performance)
Core Concepts
Dual-Context Architecture
ββββββββββββββββββββββββ postMessage ββββββββββββββββββββββββ
β MAIN THREAD β ββββββββββββββββββββΊ β UI THREAD β
β (Sandbox) β β (iframe) β
β β β β
β β figma.* API β β β DOM, fetch, canvasβ
β β Read/write nodes β β β React/HTML/CSS β
β β No browser APIs β β β No figma.* access β
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
Plugin Types
| Editor Type | Manifest Value | Read/Write | Key Capability |
|---|---|---|---|
| Figma Design | "figma" | Read + Write | Full node manipulation |
| FigJam | "figjam" | Read + Write | Stickies, connectors, stamps |
| Dev Mode | "dev" | Read Only | Inspection, codegen |
| Slides | "slides" | Read + Write | Slide manipulation |
| Buzz | "buzz" | Read + Write | Buzz-specific features |
Critical Patterns (Memorize These)
1. Immutable arrays β fills, strokes, effects MUST be cloned and reassigned:
const fills = JSON.parse(JSON.stringify(node.fills))
fills[0] = { ...fills[0], color: { r: 1, g: 0, b: 0 } }
node.fills = fills
// OR use helper: node.fills = [figma.util.solidPaint('#FF0000')]
2. Font loading β MUST load before editing any text property:
if (textNode.hasMissingFont) return // Always check first
const fonts = textNode.getRangeAllFontNames(0, textNode.characters.length)
await Promise.all(fonts.map(figma.loadFontAsync))
textNode.characters = 'New text'
3. Message passing β syntax differs between main thread and UI:
// Main β UI: figma.ui.postMessage(data)
// UI β Main: parent.postMessage({ pluginMessage: data }, '*')
4. Close plugin β MUST call when done or plugin runs forever:
figma.closePlugin() // Silent close
figma.closePlugin('Done!') // Close with notification
Workflow
Creating a New Plugin
- Choose editor type β determines manifest
editorTypeand capabilities - Set up project β TypeScript + bundler (Vite recommended)
- Design architecture β decide if plugin needs UI or runs headless
- Implement β use reference files below for API details
- Test edge cases β no selection, wrong types, missing fonts, large docs
- Publish β first publish requires review, updates are instant
Decision: Plugin vs Widget
| Need | Choose |
|---|---|
| One-time operation (export, transform, lint) | Plugin |
| Complex UI with forms/inputs | Plugin |
| External API integration | Plugin |
| Collaborative tool visible to all users | Widget |
| Persistent element on canvas | Widget |
| Real-time multiplayer features | Widget |
Decision: Build Tool
| Situation | Recommended |
|---|---|
| New project, modern stack | Vite + esbuild |
| Large codebase, many deps | Webpack + SWC |
| Simple plugin, no framework | esbuild alone |
| Want batteries-included | Create Figma Plugin (community) |
| Want hot reload dev server | Plugma (community) |
Decision: Storage
| Data Type | Store In | Scope |
|---|---|---|
| Per-document metadata | node.setPluginData() | File (your plugin) |
| Cross-plugin document data | node.setSharedPluginData() | File (all plugins) |
| User preferences | figma.clientStorage | Local machine |
| Shared/synced data | External API via fetch | Server |
Anti-Patterns
| Don't | Do Instead |
|---|---|
Modify node.fills[0] directly | Clone β modify β reassign entire array |
| Edit text without loading fonts | Always await figma.loadFontAsync() first |
Use findAll() on huge docs | Use findAllWithCriteria() (faster) |
| Load all pages at startup | Use "documentAccess": "dynamic-page", load on demand |
| Send Node objects to UI | Serialize to plain objects first |
Forget figma.closePlugin() | Always close, even in error paths |
Use getMainComponent() sync | Use getMainComponentAsync() for remote components |
| Hardcode component/style IDs | Use .key for imports, .id only within same session |
Ignore networkAccess in manifest | Specify exact domains, not wildcards |
| Skip missing font checks | Always check textNode.hasMissingFont |
Reference Files
| File | When to Load |
|---|---|
| @references/plugin-architecture.md | Setting up a new plugin, manifest questions, build tooling |
| @references/scene-graph-and-nodes.md | Working with nodes, traversal, creation, type checking |
| @references/styling-and-layout.md | Fills, strokes, effects, auto-layout, constraints |
| @references/text-and-typography.md | Text nodes, font loading, mixed styles, text manipulation |
| @references/components-and-variants.md | Components, variants, instances, library operations, styles |
| @references/ui-development.md | Plugin UI, message passing, React, theming, drag-and-drop |
| @references/variables-and-storage.md | Variables API, design tokens, pluginData, clientStorage |
| @references/devmode-codegen-publishing.md | Dev Mode, codegen, VS Code, testing, publishing, payments |
Quick Reference: Common Operations
// Selection
const sel = figma.currentPage.selection
figma.currentPage.selection = [node]
// Viewport
figma.viewport.scrollAndZoomIntoView([node])
figma.viewport.center = { x: 0, y: 0 }
figma.viewport.zoom = 1
// Notifications
figma.notify('Message')
figma.notify('Error!', { error: true })
// Events
figma.on('selectionchange', () => {})
figma.on('currentpagechange', () => {})
figma.on('documentchange', (e) => {})
figma.on('close', () => {})
figma.on('drop', (e) => {})
// Export
const bytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: 2 } })
const svg = await node.exportAsync({ format: 'SVG' })