figma-code-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +133 -0
  2. package/bin/install.js +328 -0
  3. package/knowledge/README.md +62 -0
  4. package/knowledge/css-strategy.md +973 -0
  5. package/knowledge/design-to-code-assets.md +855 -0
  6. package/knowledge/design-to-code-layout.md +929 -0
  7. package/knowledge/design-to-code-semantic.md +1085 -0
  8. package/knowledge/design-to-code-typography.md +1003 -0
  9. package/knowledge/design-to-code-visual.md +1145 -0
  10. package/knowledge/design-tokens-variables.md +1261 -0
  11. package/knowledge/design-tokens.md +960 -0
  12. package/knowledge/figma-api-devmode.md +894 -0
  13. package/knowledge/figma-api-plugin.md +920 -0
  14. package/knowledge/figma-api-rest.md +742 -0
  15. package/knowledge/figma-api-variables.md +848 -0
  16. package/knowledge/figma-api-webhooks.md +876 -0
  17. package/knowledge/payload-blocks.md +1184 -0
  18. package/knowledge/payload-figma-mapping.md +1210 -0
  19. package/knowledge/payload-visual-builder.md +1004 -0
  20. package/knowledge/plugin-architecture.md +1176 -0
  21. package/knowledge/plugin-best-practices.md +1206 -0
  22. package/knowledge/plugin-codegen.md +1313 -0
  23. package/package.json +31 -0
  24. package/skills/README.md +103 -0
  25. package/skills/audit-plugin/SKILL.md +244 -0
  26. package/skills/build-codegen-plugin/SKILL.md +279 -0
  27. package/skills/build-importer/SKILL.md +320 -0
  28. package/skills/build-plugin/SKILL.md +199 -0
  29. package/skills/build-token-pipeline/SKILL.md +363 -0
  30. package/skills/ref-html/SKILL.md +290 -0
  31. package/skills/ref-layout/SKILL.md +150 -0
  32. package/skills/ref-payload-block/SKILL.md +415 -0
  33. package/skills/ref-react/SKILL.md +222 -0
  34. package/skills/ref-tokens/SKILL.md +347 -0
@@ -0,0 +1,920 @@
1
+ # Figma Plugin API Reference
2
+
3
+ ## Purpose
4
+
5
+ Authoritative reference for the Figma Plugin API covering the sandbox execution model, SceneNode type hierarchy, IPC messaging patterns, standard plugin manifest configuration, key API methods, and plugin constraints. This module covers **standard plugins** (design-mode plugins with optional UI). For Dev Mode codegen plugins, see `figma-api-devmode.md`.
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Understand the plugin sandbox model (main thread vs UI iframe)
12
+ - Build a standard Figma plugin with a UI
13
+ - Navigate the SceneNode type hierarchy in plugin code
14
+ - Implement IPC (inter-process communication) between plugin main thread and UI
15
+ - Configure `manifest.json` for a standard plugin
16
+ - Access node properties, traverse the document tree, or create nodes
17
+ - Load fonts, export images, or store plugin data
18
+ - Understand plugin execution constraints and limitations
19
+
20
+ ---
21
+
22
+ ## Content
23
+
24
+ ### Plugin Architecture: Sandbox Model
25
+
26
+ Figma plugins use a **two-environment execution model** for security and stability.
27
+
28
+ #### Main Thread (Sandbox)
29
+
30
+ The plugin's main code (`main` entry in manifest) runs on Figma's **main thread in a sandbox**. This sandbox is a minimal JavaScript environment that:
31
+
32
+ - Has access to the full Figma document tree (read and write)
33
+ - Provides standard ES6+ JavaScript (Promise, JSON, Uint8Array, etc.)
34
+ - Provides the `figma` global object with all Plugin API methods
35
+ - Does **NOT** expose browser APIs (no DOM, no `window`, no `document`)
36
+ - Does **NOT** have `fetch()`, `XMLHttpRequest`, or any network APIs
37
+ - Does **NOT** have `setTimeout`/`setInterval` (use `figma.timer` in FigJam only)
38
+
39
+ #### UI Thread (iframe)
40
+
41
+ The plugin's UI (`ui` entry in manifest) runs in a standard browser `<iframe>`. This iframe:
42
+
43
+ - Has full access to browser APIs (DOM, `fetch`, Canvas, WebAssembly, etc.)
44
+ - Can render HTML/CSS/JavaScript UIs
45
+ - Does **NOT** have direct access to the Figma document tree or Plugin API
46
+ - Must communicate with the main thread via message passing
47
+
48
+ #### Communication Flow
49
+
50
+ ```
51
+ ┌─────────────────────────┐ message passing ┌─────────────────────────┐
52
+ │ Main Thread │ ◄─────────────────────► │ UI iframe │
53
+ │ (Sandbox) │ │ (Browser) │
54
+ │ │ │ │
55
+ │ • figma.* API access │ figma.ui.postMessage │ • DOM / HTML / CSS │
56
+ │ • Document tree R/W │ ──────────────────────► │ • fetch() / XHR │
57
+ │ • Node creation/edit │ │ • Canvas / WebGL │
58
+ │ • Style/variable access │ parent.postMessage │ • WebAssembly │
59
+ │ • No DOM, no fetch │ ◄────────────────────── │ • No figma.* API │
60
+ └─────────────────────────┘ └─────────────────────────┘
61
+ ```
62
+
63
+ ---
64
+
65
+ ### Manifest: Standard Plugins
66
+
67
+ The `manifest.json` file configures a plugin. For **standard plugins** (design-mode plugins with optional UI), the manifest uses `editorType: ["figma"]` or `["figma", "figjam"]`.
68
+
69
+ > **Critical distinction:** Standard plugins use `editorType: ["figma"]`. Dev Mode codegen plugins use `editorType: ["dev"]` with `capabilities: ["codegen"]`. These are fundamentally different plugin types. See `figma-api-devmode.md` for codegen manifests.
70
+
71
+ #### Minimal Standard Plugin Manifest
72
+
73
+ ```json
74
+ {
75
+ "name": "My Plugin",
76
+ "id": "1234567890",
77
+ "api": "1.0.0",
78
+ "main": "code.js",
79
+ "editorType": ["figma"],
80
+ "documentAccess": "dynamic-page"
81
+ }
82
+ ```
83
+
84
+ #### Full Standard Plugin Manifest
85
+
86
+ ```json
87
+ {
88
+ "name": "My Plugin",
89
+ "id": "1234567890",
90
+ "api": "1.0.0",
91
+ "main": "code.js",
92
+ "ui": "ui.html",
93
+ "editorType": ["figma", "figjam"],
94
+ "documentAccess": "dynamic-page",
95
+ "networkAccess": {
96
+ "allowedDomains": ["https://*.example.com"],
97
+ "reasoning": "Needed for syncing design tokens to server"
98
+ },
99
+ "permissions": ["currentuser"],
100
+ "menu": [
101
+ {
102
+ "name": "Export Components",
103
+ "command": "export"
104
+ },
105
+ {
106
+ "name": "Settings",
107
+ "command": "settings"
108
+ }
109
+ ],
110
+ "relaunchButtons": [
111
+ {
112
+ "command": "relaunch",
113
+ "name": "Re-export",
114
+ "multipleSelection": false
115
+ }
116
+ ],
117
+ "enableProposedApi": false
118
+ }
119
+ ```
120
+
121
+ #### Manifest Field Reference
122
+
123
+ | Field | Type | Required | Description |
124
+ |-------|------|:--------:|-------------|
125
+ | `name` | string | Yes | Plugin display name |
126
+ | `id` | string | Yes | Unique plugin ID (assigned by Figma) |
127
+ | `api` | string | Yes | API version (e.g., `"1.0.0"`) |
128
+ | `main` | string | Yes | Path to main thread JavaScript entry |
129
+ | `ui` | string or object | No | Path to UI HTML file (string) or named UI files (object) |
130
+ | `editorType` | string[] | No | `["figma"]`, `["figjam"]`, `["figma", "figjam"]`, `["dev"]`, `["slides"]`, `["buzz"]` |
131
+ | `documentAccess` | string | Yes (new plugins) | Must be `"dynamic-page"` — pages load on demand |
132
+ | `networkAccess` | object | No | `allowedDomains` array, `reasoning` string, `devAllowedDomains` array |
133
+ | `permissions` | string[] | No | `"currentuser"`, `"activeusers"`, `"fileusers"`, `"payments"`, `"teamlibrary"` |
134
+ | `capabilities` | string[] | No | `"textreview"`, `"codegen"`, `"inspect"`, `"vscode"` (for Dev Mode plugins) |
135
+ | `menu` | object[] | No | Menu items with `name`, `command`, optional nested `menu` |
136
+ | `parameters` | object[] | No | Quick action parameters with `name`, `key`, `description`, `allowFreeform`, `optional` |
137
+ | `parameterOnly` | boolean | No | If true (default when parameters exist), plugin only runs via quick actions |
138
+ | `relaunchButtons` | object[] | No | Buttons attached to nodes for re-running plugin commands |
139
+ | `enableProposedApi` | boolean | No | Enable experimental API features |
140
+ | `build` | string | No | Build command for plugin bundling |
141
+
142
+ #### editorType Rules
143
+
144
+ - `["figma"]` — Runs in Figma Design editor only
145
+ - `["figjam"]` — Runs in FigJam only
146
+ - `["figma", "figjam"]` — Runs in both Figma Design and FigJam
147
+ - `["dev"]` — Runs in Dev Mode only (for codegen/inspect plugins)
148
+ - `["slides"]` — Runs in Figma Slides only
149
+ - `["buzz"]` — Runs in Figma Buzz only
150
+ - Cannot combine `"figjam"` with `"dev"`
151
+
152
+ #### networkAccess Patterns
153
+
154
+ ```json
155
+ { "allowedDomains": ["none"] }
156
+ { "allowedDomains": ["*"] }
157
+ { "allowedDomains": ["https://api.example.com", "https://*.cdn.example.com"] }
158
+ { "allowedDomains": ["wss://realtime.example.com"] }
159
+ ```
160
+
161
+ Network restrictions apply to requests made by the plugin (fetch, XHR), not to resources loaded by websites rendered within the iframe.
162
+
163
+ ---
164
+
165
+ ### SceneNode Type Hierarchy
166
+
167
+ In Figma, every layer corresponds to a node. The Plugin API exposes these as TypeScript types.
168
+
169
+ #### Type Tree
170
+
171
+ ```
172
+ BaseNode
173
+ ├── DocumentNode
174
+ └── SceneNode
175
+ ├── FrameNode (container, optional Auto Layout)
176
+ ├── GroupNode (visual grouping, no layout properties)
177
+ ├── SectionNode (organizational section on canvas)
178
+ ├── ComponentNode (reusable component definition)
179
+ ├── ComponentSetNode (variant container)
180
+ ├── InstanceNode (instance of a component)
181
+ ├── TextNode (text layer)
182
+ ├── RectangleNode (rectangle shape)
183
+ ├── EllipseNode (ellipse/circle shape)
184
+ ├── VectorNode (arbitrary vector path)
185
+ ├── LineNode (line segment)
186
+ ├── StarNode (star shape)
187
+ ├── PolygonNode (polygon shape)
188
+ ├── BooleanOperationNode (union/subtract/intersect/exclude)
189
+ ├── SliceNode (export region, not rendered)
190
+ ├── StickyNode (FigJam only)
191
+ ├── ShapeWithTextNode (FigJam only)
192
+ ├── ConnectorNode (FigJam only)
193
+ ├── CodeBlockNode (FigJam only)
194
+ ├── TableNode (FigJam only)
195
+ ├── EmbedNode (embedded content)
196
+ ├── LinkUnfurlNode (link preview)
197
+ └── MediaNode (video/GIF)
198
+ ```
199
+
200
+ #### Key Mixins (Shared Interfaces)
201
+
202
+ Figma uses TypeScript mixins to share properties across node types:
203
+
204
+ | Mixin | Provides | Used By |
205
+ |-------|----------|---------|
206
+ | `ChildrenMixin` | `children`, `findAll()`, `findOne()`, `findChildren()`, `findAllWithCriteria()` | FrameNode, GroupNode, ComponentNode, InstanceNode, BooleanOperationNode, SectionNode |
207
+ | `LayoutMixin` | `layoutSizingHorizontal/Vertical`, `layoutGrow`, `layoutAlign`, `resize()`, `rescale()` | All SceneNodes |
208
+ | `AutoLayoutMixin` | `layoutMode`, `itemSpacing`, `paddingLeft/Right/Top/Bottom`, `primaryAxisAlignItems`, `counterAxisAlignItems`, `layoutWrap` | FrameNode, ComponentNode, ComponentSetNode, InstanceNode |
209
+ | `GeometryMixin` | `fills`, `strokes`, `strokeWeight`, `strokeAlign`, `fillGeometry`, `strokeGeometry` | FrameNode, RectangleNode, EllipseNode, VectorNode, TextNode, etc. |
210
+ | `CornerMixin` | `cornerRadius`, `cornerSmoothing` | FrameNode, RectangleNode, ComponentNode, InstanceNode |
211
+ | `RectangleCornerMixin` | `topLeftRadius`, `topRightRadius`, `bottomLeftRadius`, `bottomRightRadius` | FrameNode, RectangleNode, ComponentNode, InstanceNode |
212
+ | `BlendMixin` | `opacity`, `blendMode`, `effects`, `isMask` | All SceneNodes |
213
+ | `ExportMixin` | `exportAsync()`, `exportSettings` | All SceneNodes |
214
+ | `MinimalFillsMixin` | `fills` (read-only subset) | Some nodes |
215
+ | `MinimalStrokesMixin` | `strokes` (read-only subset) | Some nodes |
216
+
217
+ #### FrameNode vs GroupNode
218
+
219
+ This is a critical distinction for design-to-code:
220
+
221
+ | Property | FrameNode | GroupNode |
222
+ |----------|:---------:|:---------:|
223
+ | Auto Layout (`layoutMode`) | Yes | No |
224
+ | Padding properties | Yes | No |
225
+ | `itemSpacing` (gap) | Yes | No |
226
+ | `clipsContent` | Yes | No |
227
+ | `fills` (background) | Yes | No |
228
+ | `cornerRadius` | Yes | No |
229
+ | `effects` (shadows, blur) | Yes | No |
230
+ | `constraints` | Yes | No |
231
+ | Children positioning | Managed by layout | Absolute relative to group |
232
+
233
+ **Rule:** If a node has `layoutMode !== 'NONE'`, it is an Auto Layout frame and should map to a CSS flexbox container. Groups should map to a simple wrapper `<div>` with no layout styles.
234
+
235
+ #### ComponentNode vs InstanceNode
236
+
237
+ | Property | ComponentNode | InstanceNode |
238
+ |----------|:-------------:|:------------:|
239
+ | Is a definition | Yes | No (it is a usage) |
240
+ | `mainComponent` reference | N/A | Points to its ComponentNode |
241
+ | Overrides | N/A | Can override fills, text, visibility, etc. |
242
+ | Has component properties | Defines them | Consumes them |
243
+ | Can be detached | N/A | `detachInstance()` converts to FrameNode |
244
+
245
+ ```ts
246
+ // Accessing the source component from an instance
247
+ if (node.type === 'INSTANCE') {
248
+ const component = await node.getMainComponentAsync();
249
+ if (component) {
250
+ console.log('Component name:', component.name);
251
+ console.log('Component key:', component.key);
252
+ }
253
+ }
254
+ ```
255
+
256
+ ---
257
+
258
+ ### Key Plugin API Methods
259
+
260
+ #### Document and Page Access
261
+
262
+ ```ts
263
+ // Root document node
264
+ const doc: DocumentNode = figma.root;
265
+
266
+ // Current active page
267
+ const page: PageNode = figma.currentPage;
268
+
269
+ // Switch to a different page (async — loads page data)
270
+ await figma.setCurrentPageAsync(targetPage);
271
+
272
+ // Load all pages (needed if documentAccess is "dynamic-page")
273
+ await figma.loadAllPagesAsync();
274
+ ```
275
+
276
+ #### Node Retrieval
277
+
278
+ ```ts
279
+ // Get a specific node by ID (async — preferred)
280
+ const node = await figma.getNodeByIdAsync('1:2');
281
+
282
+ // Find nodes matching criteria in a subtree
283
+ const allText = page.findAll(node => node.type === 'TEXT');
284
+ const firstButton = page.findOne(node => node.name === 'Button');
285
+ const directChildren = frame.findChildren(node => node.visible);
286
+
287
+ // Find with specific criteria (more efficient)
288
+ const components = page.findAllWithCriteria({ types: ['COMPONENT'] });
289
+ ```
290
+
291
+ #### Node Creation
292
+
293
+ ```ts
294
+ // Create basic shapes
295
+ const frame = figma.createFrame();
296
+ const rect = figma.createRectangle();
297
+ const text = figma.createText();
298
+ const ellipse = figma.createEllipse();
299
+ const vector = figma.createVector();
300
+ const line = figma.createLine();
301
+
302
+ // Create components
303
+ const component = figma.createComponent();
304
+ const componentFromNode = figma.createComponentFromNode(existingNode);
305
+
306
+ // Create from SVG
307
+ const svgFrame = figma.createNodeFromSvg('<svg>...</svg>');
308
+
309
+ // Create a page
310
+ const newPage = figma.createPage();
311
+
312
+ // Create a section
313
+ const section = figma.createSection();
314
+
315
+ // Append to parent
316
+ frame.appendChild(rect);
317
+ page.appendChild(frame);
318
+ ```
319
+
320
+ #### Font Loading
321
+
322
+ Font loading is **required** before reading or modifying text content:
323
+
324
+ ```ts
325
+ // Load a specific font (MUST be called before modifying text)
326
+ await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
327
+ await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
328
+
329
+ // Now safe to modify text
330
+ textNode.characters = 'Hello World';
331
+ textNode.fontSize = 16;
332
+
333
+ // List available fonts
334
+ const fonts = await figma.listAvailableFontsAsync();
335
+ ```
336
+
337
+ > **Important:** `figma.loadFontAsync()` must be called for each font family + style combination used in a text node before modifying that node's characters or style properties. Failure to load fonts before modification throws an error.
338
+
339
+ #### Exporting Nodes
340
+
341
+ ```ts
342
+ // Export a node as PNG
343
+ const pngBytes: Uint8Array = await node.exportAsync({
344
+ format: 'PNG',
345
+ constraint: { type: 'SCALE', value: 2 }
346
+ });
347
+
348
+ // Export as SVG
349
+ const svgBytes: Uint8Array = await node.exportAsync({
350
+ format: 'SVG',
351
+ svgOutlineText: true,
352
+ svgIdAttribute: false
353
+ });
354
+
355
+ // Export as PDF
356
+ const pdfBytes: Uint8Array = await node.exportAsync({
357
+ format: 'PDF'
358
+ });
359
+
360
+ // Export as JPG
361
+ const jpgBytes: Uint8Array = await node.exportAsync({
362
+ format: 'JPG',
363
+ constraint: { type: 'SCALE', value: 1 }
364
+ });
365
+ ```
366
+
367
+ #### Plugin Data Storage
368
+
369
+ ```ts
370
+ // Store data on a node (private to this plugin)
371
+ node.setPluginData('metadata', JSON.stringify({ exported: true }));
372
+
373
+ // Read data from a node
374
+ const data = node.getPluginData('metadata');
375
+
376
+ // Store data shared across plugins
377
+ node.setSharedPluginData('namespace', 'key', 'value');
378
+ const shared = node.getSharedPluginData('namespace', 'key');
379
+
380
+ // Persistent client-side storage (survives plugin restarts)
381
+ await figma.clientStorage.setAsync('settings', { theme: 'dark' });
382
+ const settings = await figma.clientStorage.getAsync('settings');
383
+ ```
384
+
385
+ #### Styles and Variables (Plugin API)
386
+
387
+ ```ts
388
+ // Get local styles
389
+ const paintStyles = await figma.getLocalPaintStylesAsync();
390
+ const textStyles = await figma.getLocalTextStylesAsync();
391
+ const effectStyles = await figma.getLocalEffectStylesAsync();
392
+
393
+ // Get a style by ID
394
+ const style = await figma.getStyleByIdAsync('S:abc123');
395
+
396
+ // Import a published style by key
397
+ const importedStyle = await figma.importStyleByKeyAsync('component-key');
398
+
399
+ // Import a published component by key
400
+ const component = await figma.importComponentByKeyAsync('component-key');
401
+
402
+ // Access local variables (Plugin API — works on all plans)
403
+ const variables = await figma.variables.getLocalVariablesAsync();
404
+ const collections = await figma.variables.getLocalVariableCollectionsAsync();
405
+ ```
406
+
407
+ > **Note:** The Plugin API `figma.variables` gives access to local variables on ALL Figma plans. The REST API Variables endpoints require Enterprise. This makes the Plugin API the preferred fallback for variable access on non-Enterprise plans.
408
+
409
+ #### Events
410
+
411
+ ```ts
412
+ // Run event — fired when plugin starts
413
+ figma.on('run', (event: RunEvent) => {
414
+ console.log('Command:', event.command);
415
+ console.log('Parameters:', event.parameters);
416
+ });
417
+
418
+ // Selection change
419
+ figma.on('selectionchange', () => {
420
+ const selection = figma.currentPage.selection;
421
+ console.log('Selected:', selection.length, 'nodes');
422
+ });
423
+
424
+ // Document change
425
+ figma.on('documentchange', (event: DocumentChangeEvent) => {
426
+ for (const change of event.documentChanges) {
427
+ console.log('Changed:', change.type, change.id);
428
+ }
429
+ });
430
+
431
+ // Close event — cleanup before plugin closes
432
+ figma.on('close', () => {
433
+ // Perform cleanup
434
+ });
435
+
436
+ // Drop event — handle drag-and-drop onto canvas
437
+ figma.on('drop', (event: DropEvent) => {
438
+ const { items, node, dropMetadata } = event;
439
+ // Handle dropped items
440
+ return false; // return true to prevent default behavior
441
+ });
442
+ ```
443
+
444
+ #### UI Management
445
+
446
+ ```ts
447
+ // Show the UI iframe
448
+ figma.showUI(__html__, {
449
+ width: 400,
450
+ height: 600,
451
+ title: 'My Plugin',
452
+ visible: true,
453
+ themeColors: true // Use Figma's theme colors
454
+ });
455
+
456
+ // Show named UI (when ui is an object in manifest)
457
+ figma.showUI(__uiFiles__['settings'], { width: 300, height: 200 });
458
+
459
+ // Resize after creation
460
+ figma.ui.resize(500, 700);
461
+
462
+ // Reposition
463
+ figma.ui.reposition(100, 100);
464
+
465
+ // Hide without destroying
466
+ figma.ui.hide();
467
+ figma.ui.show();
468
+
469
+ // Close and destroy
470
+ figma.ui.close();
471
+
472
+ // Close the entire plugin
473
+ figma.closePlugin('Done!'); // Optional message shown to user
474
+ ```
475
+
476
+ #### Utilities
477
+
478
+ ```ts
479
+ // Show a notification toast
480
+ figma.notify('Export complete!', { timeout: 3000 });
481
+ figma.notify('Error occurred', { error: true });
482
+
483
+ // Open external URL (requires user confirmation)
484
+ figma.openExternal('https://example.com');
485
+
486
+ // Base64 encode/decode
487
+ const encoded = figma.base64Encode(uint8Array);
488
+ const decoded = figma.base64Decode(encodedString);
489
+
490
+ // Create an image from bytes
491
+ const image = figma.createImage(pngBytes);
492
+
493
+ // Create an image from URL (fetched by Figma)
494
+ const image = await figma.createImageAsync('https://example.com/image.png');
495
+
496
+ // Save version history
497
+ await figma.saveVersionHistoryAsync('Auto-export v2', 'Exported all components');
498
+ ```
499
+
500
+ ---
501
+
502
+ ### IPC Messaging Patterns
503
+
504
+ Communication between the main thread and UI iframe uses structured message passing.
505
+
506
+ #### Basic Pattern: Main Thread to UI
507
+
508
+ ```ts
509
+ // Main thread (code.ts)
510
+ figma.showUI(__html__);
511
+
512
+ // Send data to UI
513
+ figma.ui.postMessage({
514
+ type: 'NODE_DATA',
515
+ payload: {
516
+ name: node.name,
517
+ width: node.width,
518
+ height: node.height
519
+ }
520
+ });
521
+ ```
522
+
523
+ ```html
524
+ <!-- UI (ui.html) -->
525
+ <script>
526
+ window.onmessage = (event) => {
527
+ const msg = event.data.pluginMessage;
528
+ if (msg.type === 'NODE_DATA') {
529
+ document.getElementById('name').textContent = msg.payload.name;
530
+ }
531
+ };
532
+ </script>
533
+ ```
534
+
535
+ #### Basic Pattern: UI to Main Thread
536
+
537
+ ```html
538
+ <!-- UI (ui.html) -->
539
+ <script>
540
+ function exportSelection() {
541
+ parent.postMessage({
542
+ pluginMessage: {
543
+ type: 'EXPORT',
544
+ format: 'svg',
545
+ scale: 2
546
+ }
547
+ }, '*');
548
+ }
549
+ </script>
550
+ ```
551
+
552
+ ```ts
553
+ // Main thread (code.ts)
554
+ figma.ui.onmessage = (msg) => {
555
+ if (msg.type === 'EXPORT') {
556
+ // Handle export request
557
+ const bytes = await node.exportAsync({ format: msg.format });
558
+ figma.ui.postMessage({ type: 'EXPORT_RESULT', data: bytes });
559
+ }
560
+ };
561
+
562
+ // Alternative: multiple handlers with .on()
563
+ figma.ui.on('message', (msg) => {
564
+ if (msg.type === 'EXPORT') { /* ... */ }
565
+ });
566
+ ```
567
+
568
+ #### Pattern: Request/Response with Correlation IDs
569
+
570
+ For complex plugins with concurrent operations, use correlation IDs to match responses:
571
+
572
+ ```ts
573
+ // Main thread (code.ts)
574
+ figma.ui.on('message', async (msg) => {
575
+ if (msg.type === 'FETCH_REQUEST') {
576
+ // Process the request
577
+ const nodes = figma.currentPage.findAll(n => n.type === msg.nodeType);
578
+ const result = nodes.map(n => ({ id: n.id, name: n.name }));
579
+
580
+ figma.ui.postMessage({
581
+ type: 'FETCH_RESPONSE',
582
+ requestId: msg.requestId, // Echo the correlation ID
583
+ payload: result
584
+ });
585
+ }
586
+ });
587
+ ```
588
+
589
+ ```html
590
+ <!-- UI (ui.html) -->
591
+ <script>
592
+ let nextRequestId = 1;
593
+ const pendingRequests = new Map();
594
+
595
+ function fetchNodes(nodeType) {
596
+ return new Promise((resolve) => {
597
+ const requestId = nextRequestId++;
598
+ pendingRequests.set(requestId, resolve);
599
+ parent.postMessage({
600
+ pluginMessage: {
601
+ type: 'FETCH_REQUEST',
602
+ requestId,
603
+ nodeType
604
+ }
605
+ }, '*');
606
+ });
607
+ }
608
+
609
+ window.onmessage = (event) => {
610
+ const msg = event.data.pluginMessage;
611
+ if (msg.type === 'FETCH_RESPONSE' && pendingRequests.has(msg.requestId)) {
612
+ pendingRequests.get(msg.requestId)(msg.payload);
613
+ pendingRequests.delete(msg.requestId);
614
+ }
615
+ };
616
+ </script>
617
+ ```
618
+
619
+ #### Pattern: Network Proxy (fetch via UI)
620
+
621
+ Since the main thread cannot make network requests, proxy them through the UI:
622
+
623
+ ```ts
624
+ // Main thread (code.ts)
625
+ figma.ui.postMessage({
626
+ type: 'API_REQUEST',
627
+ url: 'https://api.example.com/tokens',
628
+ method: 'POST',
629
+ headers: { 'Content-Type': 'application/json' },
630
+ body: JSON.stringify(tokenData)
631
+ });
632
+
633
+ figma.ui.on('message', (msg) => {
634
+ if (msg.type === 'API_RESPONSE') {
635
+ if (msg.ok) {
636
+ figma.notify('Tokens synced!');
637
+ } else {
638
+ figma.notify('Sync failed: ' + msg.error, { error: true });
639
+ }
640
+ }
641
+ });
642
+ ```
643
+
644
+ ```html
645
+ <!-- UI (ui.html) -->
646
+ <script>
647
+ window.onmessage = async (event) => {
648
+ const msg = event.data.pluginMessage;
649
+ if (msg.type === 'API_REQUEST') {
650
+ try {
651
+ const res = await fetch(msg.url, {
652
+ method: msg.method,
653
+ headers: msg.headers,
654
+ body: msg.body
655
+ });
656
+ const data = await res.json();
657
+ parent.postMessage({
658
+ pluginMessage: {
659
+ type: 'API_RESPONSE',
660
+ ok: res.ok,
661
+ status: res.status,
662
+ data
663
+ }
664
+ }, '*');
665
+ } catch (err) {
666
+ parent.postMessage({
667
+ pluginMessage: {
668
+ type: 'API_RESPONSE',
669
+ ok: false,
670
+ error: err.message
671
+ }
672
+ }, '*');
673
+ }
674
+ }
675
+ };
676
+ </script>
677
+ ```
678
+
679
+ ---
680
+
681
+ ### Common Plugin Patterns
682
+
683
+ #### Walking the Node Tree
684
+
685
+ ```ts
686
+ function walkTree(node: BaseNode, callback: (node: BaseNode) => void): void {
687
+ callback(node);
688
+ if ('children' in node) {
689
+ for (const child of node.children) {
690
+ walkTree(child, callback);
691
+ }
692
+ }
693
+ }
694
+
695
+ // Usage: collect all text content
696
+ const textNodes: TextNode[] = [];
697
+ walkTree(figma.currentPage, (node) => {
698
+ if (node.type === 'TEXT') {
699
+ textNodes.push(node as TextNode);
700
+ }
701
+ });
702
+ ```
703
+
704
+ #### Reading Auto Layout Properties
705
+
706
+ ```ts
707
+ function getLayoutInfo(node: FrameNode | ComponentNode | InstanceNode) {
708
+ if (node.layoutMode === 'NONE') {
709
+ return { isAutoLayout: false };
710
+ }
711
+
712
+ return {
713
+ isAutoLayout: true,
714
+ direction: node.layoutMode, // 'HORIZONTAL' | 'VERTICAL'
715
+ gap: node.itemSpacing, // number (maps to CSS gap)
716
+ wrap: node.layoutWrap, // 'NO_WRAP' | 'WRAP'
717
+ padding: {
718
+ top: node.paddingTop,
719
+ right: node.paddingRight,
720
+ bottom: node.paddingBottom,
721
+ left: node.paddingLeft,
722
+ },
723
+ primaryAlign: node.primaryAxisAlignItems, // 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN'
724
+ crossAlign: node.counterAxisAlignItems, // 'MIN' | 'CENTER' | 'MAX' | 'BASELINE'
725
+ crossContentAlign: node.counterAxisAlignContent, // 'AUTO' | 'SPACE_BETWEEN'
726
+ sizingH: node.layoutSizingHorizontal, // 'FIXED' | 'HUG' | 'FILL'
727
+ sizingV: node.layoutSizingVertical, // 'FIXED' | 'HUG' | 'FILL'
728
+ };
729
+ }
730
+ ```
731
+
732
+ #### Reading Visual Properties
733
+
734
+ ```ts
735
+ function getVisualInfo(node: SceneNode) {
736
+ const info: Record<string, any> = {
737
+ opacity: 'opacity' in node ? node.opacity : 1,
738
+ blendMode: 'blendMode' in node ? node.blendMode : 'NORMAL',
739
+ visible: node.visible,
740
+ };
741
+
742
+ // Fills
743
+ if ('fills' in node && node.fills !== figma.mixed) {
744
+ info.fills = (node.fills as readonly Paint[]).filter(f => f.visible !== false);
745
+ }
746
+
747
+ // Strokes
748
+ if ('strokes' in node) {
749
+ info.strokes = node.strokes;
750
+ info.strokeWeight = 'strokeWeight' in node ? node.strokeWeight : 0;
751
+ info.strokeAlign = 'strokeAlign' in node ? node.strokeAlign : 'CENTER';
752
+ }
753
+
754
+ // Corner radius
755
+ if ('cornerRadius' in node) {
756
+ if (node.cornerRadius !== figma.mixed) {
757
+ info.cornerRadius = node.cornerRadius;
758
+ } else if ('topLeftRadius' in node) {
759
+ info.cornerRadius = [
760
+ node.topLeftRadius,
761
+ node.topRightRadius,
762
+ node.bottomRightRadius,
763
+ node.bottomLeftRadius
764
+ ];
765
+ }
766
+ }
767
+
768
+ // Effects (shadows, blur)
769
+ if ('effects' in node) {
770
+ info.effects = node.effects.filter(e => e.visible !== false);
771
+ }
772
+
773
+ return info;
774
+ }
775
+ ```
776
+
777
+ #### Processing Component Instances
778
+
779
+ ```ts
780
+ async function processInstances(page: PageNode) {
781
+ const instances = page.findAllWithCriteria({ types: ['INSTANCE'] }) as InstanceNode[];
782
+
783
+ for (const instance of instances) {
784
+ const mainComponent = await instance.getMainComponentAsync();
785
+
786
+ if (!mainComponent) {
787
+ console.log(`${instance.name}: detached or missing component`);
788
+ continue;
789
+ }
790
+
791
+ // Check if from external library
792
+ const isRemote = mainComponent.remote;
793
+ const componentKey = mainComponent.key;
794
+ const componentName = mainComponent.name;
795
+
796
+ // Access overridden properties
797
+ const overrides = instance.overrides;
798
+
799
+ console.log(`Instance "${instance.name}" → Component "${componentName}" (remote: ${isRemote})`);
800
+ }
801
+ }
802
+ ```
803
+
804
+ ---
805
+
806
+ ### Plugin Constraints and Limitations
807
+
808
+ #### Execution Constraints
809
+
810
+ | Constraint | Details |
811
+ |-----------|---------|
812
+ | **Single plugin at a time** | Users can only run one plugin and one widget simultaneously |
813
+ | **No background execution** | Plugins cannot run in the background; they must be actively invoked |
814
+ | **Plugin lifecycle** | Must call `figma.closePlugin()` to terminate (unless using `figma.on('close', ...)`) |
815
+ | **Dynamic page loading** | Pages load on demand (`documentAccess: "dynamic-page"`). Use `figma.loadAllPagesAsync()` if you need all pages |
816
+ | **Asynchronous operations** | Font loading, page switching, image creation, and many node lookups are async |
817
+
818
+ #### Main Thread Sandbox Restrictions
819
+
820
+ | Restriction | Details |
821
+ |------------|---------|
822
+ | **No DOM access** | `document`, `window`, `HTMLElement`, etc. are not available |
823
+ | **No network access** | `fetch()`, `XMLHttpRequest`, WebSocket are not available — proxy through UI iframe |
824
+ | **No timers** | `setTimeout`, `setInterval` are not available in the main sandbox (except FigJam `figma.timer`) |
825
+ | **No external fonts** | Can only load fonts available in the Figma editor via `figma.loadFontAsync()` |
826
+ | **No file metadata** | Cannot access team info, permissions, comments, or version history (except `figma.saveVersionHistoryAsync()`) |
827
+
828
+ #### Data and Access Limitations
829
+
830
+ | Limitation | Details |
831
+ |-----------|---------|
832
+ | **Library access** | Cannot access library styles/components unless imported into the current file |
833
+ | **Font requirement** | Must call `figma.loadFontAsync()` before modifying any text node's characters or style |
834
+ | **Image data** | Can create images from bytes or URL, but cannot read pixel data from existing image fills directly |
835
+ | **Plugin data scope** | `setPluginData()` is private to the plugin ID; use `setSharedPluginData()` for cross-plugin data |
836
+ | **Client storage** | `figma.clientStorage` is local to the user's machine and plugin ID |
837
+
838
+ #### Performance Considerations
839
+
840
+ - Avoid calling `figma.root.findAll()` on large files — use page-level searches or `findAllWithCriteria()` for type-filtered searches
841
+ - Batch node modifications before yielding control to reduce re-renders
842
+ - Use `figma.skipInvisibleInstanceChildren = true` to skip hidden instance children during traversal
843
+ - Prefer async API variants (`getNodeByIdAsync`, `getLocalPaintStylesAsync`, etc.) — sync versions are deprecated
844
+
845
+ ---
846
+
847
+ ### @create-figma-plugin Tooling
848
+
849
+ The `@create-figma-plugin` package is a popular open-source toolkit for building Figma plugins with modern tooling:
850
+
851
+ ```bash
852
+ npx create-figma-plugin
853
+ ```
854
+
855
+ Key features:
856
+ - TypeScript support out of the box
857
+ - Preact-based UI components matching Figma's design
858
+ - Build system with esbuild
859
+ - Module-based plugin structure (separates handler and UI)
860
+ - Automatic manifest generation from package.json
861
+
862
+ **Handler pattern:**
863
+
864
+ ```ts
865
+ // src/main.ts
866
+ import { on, emit, showUI } from '@create-figma-plugin/utilities';
867
+
868
+ export default function () {
869
+ on('EXPORT_REQUEST', async (format: string) => {
870
+ const selection = figma.currentPage.selection;
871
+ // Process selection...
872
+ emit('EXPORT_RESULT', { success: true, count: selection.length });
873
+ });
874
+
875
+ showUI({ width: 400, height: 300 });
876
+ }
877
+ ```
878
+
879
+ ```tsx
880
+ // src/ui.tsx
881
+ import { render, useWindowResize } from '@create-figma-plugin/ui';
882
+ import { emit, on } from '@create-figma-plugin/utilities';
883
+ import { h } from 'preact';
884
+ import { useState } from 'preact/hooks';
885
+
886
+ function Plugin() {
887
+ const [result, setResult] = useState(null);
888
+
889
+ on('EXPORT_RESULT', (data) => setResult(data));
890
+
891
+ function handleExport() {
892
+ emit('EXPORT_REQUEST', 'svg');
893
+ }
894
+
895
+ return (
896
+ <div>
897
+ <button onclick={handleExport}>Export</button>
898
+ {result && <p>Exported {result.count} nodes</p>}
899
+ </div>
900
+ );
901
+ }
902
+
903
+ export default render(Plugin);
904
+ ```
905
+
906
+ The `emit`/`on` helpers from `@create-figma-plugin/utilities` abstract the raw `postMessage`/`onmessage` IPC pattern into a typed event system.
907
+
908
+ ---
909
+
910
+ ## Cross-References
911
+
912
+ - **`figma-api-rest.md`** — REST API for file/node fetching from outside Figma (authentication, endpoints, rate limits)
913
+ - **`figma-api-devmode.md`** — Dev Mode codegen plugins (different manifest, different lifecycle, `figma.codegen` API)
914
+ - **`figma-api-variables.md`** — Variables REST API for design tokens (Enterprise only; Plugin API `figma.variables` works on all plans)
915
+ - **`figma-api-webhooks.md`** — Webhooks v2 for reacting to file changes externally
916
+ - **`design-to-code-layout.md`** — Auto Layout to Flexbox mapping (consumes layout properties from Plugin API)
917
+ - **`design-to-code-visual.md`** — Visual property extraction patterns (fills, strokes, effects)
918
+ - **`design-to-code-assets.md`** — Asset export patterns using `node.exportAsync()`
919
+ - **`plugin-architecture.md`** — Production plugin architecture patterns (project setup, IPC design, data flow pipeline)
920
+ - **`plugin-best-practices.md`** — Best practices for Figma plugin development