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.
- package/README.md +133 -0
- package/bin/install.js +328 -0
- package/knowledge/README.md +62 -0
- package/knowledge/css-strategy.md +973 -0
- package/knowledge/design-to-code-assets.md +855 -0
- package/knowledge/design-to-code-layout.md +929 -0
- package/knowledge/design-to-code-semantic.md +1085 -0
- package/knowledge/design-to-code-typography.md +1003 -0
- package/knowledge/design-to-code-visual.md +1145 -0
- package/knowledge/design-tokens-variables.md +1261 -0
- package/knowledge/design-tokens.md +960 -0
- package/knowledge/figma-api-devmode.md +894 -0
- package/knowledge/figma-api-plugin.md +920 -0
- package/knowledge/figma-api-rest.md +742 -0
- package/knowledge/figma-api-variables.md +848 -0
- package/knowledge/figma-api-webhooks.md +876 -0
- package/knowledge/payload-blocks.md +1184 -0
- package/knowledge/payload-figma-mapping.md +1210 -0
- package/knowledge/payload-visual-builder.md +1004 -0
- package/knowledge/plugin-architecture.md +1176 -0
- package/knowledge/plugin-best-practices.md +1206 -0
- package/knowledge/plugin-codegen.md +1313 -0
- package/package.json +31 -0
- package/skills/README.md +103 -0
- package/skills/audit-plugin/SKILL.md +244 -0
- package/skills/build-codegen-plugin/SKILL.md +279 -0
- package/skills/build-importer/SKILL.md +320 -0
- package/skills/build-plugin/SKILL.md +199 -0
- package/skills/build-token-pipeline/SKILL.md +363 -0
- package/skills/ref-html/SKILL.md +290 -0
- package/skills/ref-layout/SKILL.md +150 -0
- package/skills/ref-payload-block/SKILL.md +415 -0
- package/skills/ref-react/SKILL.md +222 -0
- package/skills/ref-tokens/SKILL.md +347 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
# Figma Variables API Reference
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Authoritative reference for the Figma Variables REST API covering access requirements, endpoints, the variable data model (Variable, VariableCollection, modes, scopes), variable resolution mechanics, multi-mode theming patterns, and practical token extraction strategies. This is the design tokens foundation that all token-related modules depend on.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reference this module when you need to:
|
|
10
|
+
|
|
11
|
+
- Extract design tokens from Figma files via the Variables API
|
|
12
|
+
- Understand variable collections, modes, and the `valuesByMode` structure
|
|
13
|
+
- Resolve variable aliases and bound variables on nodes
|
|
14
|
+
- Implement multi-mode theming (light/dark, brand variants)
|
|
15
|
+
- Build token dictionaries from Figma Variables for CSS/Tailwind/platform export
|
|
16
|
+
- Determine access requirements for Variables API endpoints
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Content
|
|
21
|
+
|
|
22
|
+
### Access Requirements
|
|
23
|
+
|
|
24
|
+
The Variables REST API is restricted to **full members of Enterprise organizations**. Guests and members of non-Enterprise plans cannot access these endpoints.
|
|
25
|
+
|
|
26
|
+
| Requirement | Details |
|
|
27
|
+
|------------|---------|
|
|
28
|
+
| **Figma plan** | Enterprise |
|
|
29
|
+
| **Seat type** | Full member (not guest) |
|
|
30
|
+
| **Read scope** | `file_variables:read` |
|
|
31
|
+
| **Write scope** | `file_variables:write` |
|
|
32
|
+
| **Read permission** | View access to the file |
|
|
33
|
+
| **Write permission** | Edit access to the file |
|
|
34
|
+
|
|
35
|
+
> **Important:** If your organization is not on an Enterprise plan, you must extract tokens by traversing the file tree and reading style properties from nodes. See `figma-api-rest.md` for file traversal patterns.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### Endpoints
|
|
40
|
+
|
|
41
|
+
#### GET /v1/files/:file_key/variables/local
|
|
42
|
+
|
|
43
|
+
Retrieves all local variables and collections created in the file, plus any remote variables consumed by the file.
|
|
44
|
+
|
|
45
|
+
**Scope required:** `file_variables:read`
|
|
46
|
+
**Rate limit tier:** Tier 2
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
|
|
50
|
+
"https://api.figma.com/v1/files/FILE_KEY/variables/local"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Response shape:**
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
interface GetLocalVariablesResponse {
|
|
57
|
+
status: number;
|
|
58
|
+
error: boolean;
|
|
59
|
+
meta: {
|
|
60
|
+
variables: Record<string, Variable>;
|
|
61
|
+
variableCollections: Record<string, VariableCollection>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This endpoint returns:
|
|
67
|
+
- All variables defined locally in the file
|
|
68
|
+
- All remote (external library) variables that are referenced/used in the file
|
|
69
|
+
- All variable collections (local and remote)
|
|
70
|
+
|
|
71
|
+
Use the `remote` boolean field on each variable/collection to distinguish local from external.
|
|
72
|
+
|
|
73
|
+
#### GET /v1/files/:file_key/variables/published
|
|
74
|
+
|
|
75
|
+
Retrieves variables that have been published from the specified file (for library consumers).
|
|
76
|
+
|
|
77
|
+
**Scope required:** `file_variables:read`
|
|
78
|
+
**Rate limit tier:** Tier 2
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl -H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
|
|
82
|
+
"https://api.figma.com/v1/files/FILE_KEY/variables/published"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Key differences from `/local`:**
|
|
86
|
+
|
|
87
|
+
- Returns only published variables (not local-only or unpublished)
|
|
88
|
+
- Each variable and collection includes a `subscribed_id` field for consumer tracking
|
|
89
|
+
- Mode details are omitted from the response
|
|
90
|
+
|
|
91
|
+
Use this endpoint when building tooling for library consumers who need to reference published tokens.
|
|
92
|
+
|
|
93
|
+
#### POST /v1/files/:file_key/variables
|
|
94
|
+
|
|
95
|
+
Bulk create, update, and delete variables, collections, and modes.
|
|
96
|
+
|
|
97
|
+
**Scope required:** `file_variables:write`
|
|
98
|
+
**Rate limit tier:** Tier 3
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
curl -X POST \
|
|
102
|
+
-H "X-Figma-Token: YOUR_FIGMA_TOKEN" \
|
|
103
|
+
-H "Content-Type: application/json" \
|
|
104
|
+
-d '{ "variableCollections": [...], "variables": [...] }' \
|
|
105
|
+
"https://api.figma.com/v1/files/FILE_KEY/variables"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Request body arrays (processed in order):**
|
|
109
|
+
|
|
110
|
+
| Array | Purpose |
|
|
111
|
+
|-------|---------|
|
|
112
|
+
| `variableCollections` | Create, update, or delete collections |
|
|
113
|
+
| `variableModes` | Create, update, or delete modes |
|
|
114
|
+
| `variables` | Create, update, or delete variables |
|
|
115
|
+
| `variableModeValues` | Set variable values per mode |
|
|
116
|
+
|
|
117
|
+
**Constraints:**
|
|
118
|
+
|
|
119
|
+
| Constraint | Limit |
|
|
120
|
+
|-----------|-------|
|
|
121
|
+
| Max modes per collection | 40 |
|
|
122
|
+
| Max variables per collection | 5,000 |
|
|
123
|
+
| Max request payload | 4 MB |
|
|
124
|
+
| Mode name length | 40 characters |
|
|
125
|
+
| Variable name restrictions | Cannot contain `.`, `{`, `}` |
|
|
126
|
+
| Variable name uniqueness | Must be unique within collection |
|
|
127
|
+
|
|
128
|
+
**Atomicity:** All changes in a single POST request succeed or fail together. Partial application does not occur.
|
|
129
|
+
|
|
130
|
+
**Temporary IDs:** When creating new objects, you can use temporary IDs (prefixed with any string) to reference objects created in the same request. For example, create a collection and immediately add variables to it.
|
|
131
|
+
|
|
132
|
+
> **Important:** Variables created or updated via the REST API must be **published** before they become accessible in other files. The API does not auto-publish.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### Variable Data Model
|
|
137
|
+
|
|
138
|
+
#### Variable
|
|
139
|
+
|
|
140
|
+
A single design token that defines values for each mode in its parent collection.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
interface Variable {
|
|
144
|
+
/** Unique identifier within the file */
|
|
145
|
+
id: string;
|
|
146
|
+
|
|
147
|
+
/** Variable name (e.g., "primary/500", "spacing/md") */
|
|
148
|
+
name: string;
|
|
149
|
+
|
|
150
|
+
/** Stable key for cross-file references */
|
|
151
|
+
key: string;
|
|
152
|
+
|
|
153
|
+
/** Parent collection ID */
|
|
154
|
+
variableCollectionId: string;
|
|
155
|
+
|
|
156
|
+
/** Resolved type of this variable's values */
|
|
157
|
+
resolvedType: 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR';
|
|
158
|
+
|
|
159
|
+
/** Values indexed by mode ID */
|
|
160
|
+
valuesByMode: Record<string, VariableValue>;
|
|
161
|
+
|
|
162
|
+
/** Whether this variable comes from an external library */
|
|
163
|
+
remote: boolean;
|
|
164
|
+
|
|
165
|
+
/** Human-readable description */
|
|
166
|
+
description: string;
|
|
167
|
+
|
|
168
|
+
/** If true, excluded from library publishing */
|
|
169
|
+
hiddenFromPublishing: boolean;
|
|
170
|
+
|
|
171
|
+
/** Controls which UI pickers show this variable */
|
|
172
|
+
scopes: VariableScope[];
|
|
173
|
+
|
|
174
|
+
/** Platform-specific code syntax for Dev Mode */
|
|
175
|
+
codeSyntax: VariableCodeSyntax;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* True if the variable was deleted but is still referenced
|
|
179
|
+
* by other variables or nodes in the file
|
|
180
|
+
*/
|
|
181
|
+
deletedButReferenced?: boolean;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### VariableCollection
|
|
186
|
+
|
|
187
|
+
A grouping of related variables that share the same set of modes.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
interface VariableCollection {
|
|
191
|
+
/** Unique identifier within the file */
|
|
192
|
+
id: string;
|
|
193
|
+
|
|
194
|
+
/** Collection name (e.g., "Colors", "Spacing", "Typography") */
|
|
195
|
+
name: string;
|
|
196
|
+
|
|
197
|
+
/** Stable key for cross-file references */
|
|
198
|
+
key: string;
|
|
199
|
+
|
|
200
|
+
/** Modes defined in this collection: { modeId: modeName } */
|
|
201
|
+
modes: Record<string, string>;
|
|
202
|
+
|
|
203
|
+
/** The default mode ID (used when no explicit mode is set) */
|
|
204
|
+
defaultModeId: string;
|
|
205
|
+
|
|
206
|
+
/** Whether this collection comes from an external library */
|
|
207
|
+
remote: boolean;
|
|
208
|
+
|
|
209
|
+
/** If true, excluded from library publishing */
|
|
210
|
+
hiddenFromPublishing: boolean;
|
|
211
|
+
|
|
212
|
+
/** Ordered list of variable IDs in this collection */
|
|
213
|
+
variableIds: string[];
|
|
214
|
+
|
|
215
|
+
/** Whether this is an extended (inherited) collection */
|
|
216
|
+
isExtension?: boolean;
|
|
217
|
+
|
|
218
|
+
/** Parent collection ID (for extended collections) */
|
|
219
|
+
parentVariableCollectionId?: string;
|
|
220
|
+
|
|
221
|
+
/** Root collection ID in the extension chain */
|
|
222
|
+
rootVariableCollectionId?: string;
|
|
223
|
+
|
|
224
|
+
/** Variable IDs inherited from parent (extended collections) */
|
|
225
|
+
inheritedVariableIds?: string[];
|
|
226
|
+
|
|
227
|
+
/** Variable IDs created locally (extended collections) */
|
|
228
|
+
localVariableIds?: string[];
|
|
229
|
+
|
|
230
|
+
/** Override values for inherited variables */
|
|
231
|
+
variableOverrides?: Record<string, any>;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### VariableValue
|
|
236
|
+
|
|
237
|
+
A variable's value for a given mode is either a direct value or an alias to another variable.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
/** Direct values based on resolvedType */
|
|
241
|
+
type VariableValue =
|
|
242
|
+
| boolean // BOOLEAN
|
|
243
|
+
| number // FLOAT
|
|
244
|
+
| string // STRING
|
|
245
|
+
| RGBA // COLOR
|
|
246
|
+
| VariableAlias; // Reference to another variable
|
|
247
|
+
|
|
248
|
+
interface RGBA {
|
|
249
|
+
r: number; // 0-1
|
|
250
|
+
g: number; // 0-1
|
|
251
|
+
b: number; // 0-1
|
|
252
|
+
a: number; // 0-1
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface VariableAlias {
|
|
256
|
+
type: 'VARIABLE_ALIAS';
|
|
257
|
+
id: string; // The referenced variable's ID
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
> **Note:** Color values use 0-1 range, NOT 0-255. Multiply by 255 and round when converting to hex/rgb CSS values.
|
|
262
|
+
|
|
263
|
+
#### VariableScope
|
|
264
|
+
|
|
265
|
+
Scopes control which property pickers in the Figma UI show the variable. Scopes do NOT prevent programmatic binding via the API or plugin code.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
type VariableScope =
|
|
269
|
+
| 'ALL_SCOPES' // Shown in all pickers (if set, no other scopes allowed)
|
|
270
|
+
| 'TEXT_CONTENT' // String variables: text content
|
|
271
|
+
| 'CORNER_RADIUS' // Float variables: border radius
|
|
272
|
+
| 'WIDTH_HEIGHT' // Float variables: width/height
|
|
273
|
+
| 'GAP' // Float variables: auto layout gap/spacing
|
|
274
|
+
| 'ALL_FILLS' // Color variables: all fill types
|
|
275
|
+
| 'FRAME_FILL' // Color variables: frame fills only
|
|
276
|
+
| 'SHAPE_FILL' // Color variables: shape fills only
|
|
277
|
+
| 'TEXT_FILL' // Color variables: text fills only
|
|
278
|
+
| 'STROKE_COLOR' // Color variables: stroke colors
|
|
279
|
+
| 'EFFECT_COLOR' // Color variables: effect colors (shadows, etc.)
|
|
280
|
+
| 'STROKE_FLOAT' // Float variables: stroke width
|
|
281
|
+
| 'EFFECT_FLOAT' // Float variables: effect values (blur, spread)
|
|
282
|
+
| 'OPACITY' // Float variables: opacity
|
|
283
|
+
| 'FONT_FAMILY' // String variables: font family
|
|
284
|
+
| 'FONT_STYLE' // String variables: font style
|
|
285
|
+
| 'FONT_WEIGHT' // Float variables: font weight
|
|
286
|
+
| 'FONT_SIZE' // Float variables: font size
|
|
287
|
+
| 'LINE_HEIGHT' // Float variables: line height
|
|
288
|
+
| 'LETTER_SPACING' // Float variables: letter spacing
|
|
289
|
+
| 'PARAGRAPH_SPACING' // Float variables: paragraph spacing
|
|
290
|
+
| 'PARAGRAPH_INDENT' // Float variables: paragraph indent
|
|
291
|
+
;
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Scope validity by resolvedType:**
|
|
295
|
+
|
|
296
|
+
| resolvedType | Valid Scopes |
|
|
297
|
+
|-------------|-------------|
|
|
298
|
+
| `BOOLEAN` | `ALL_SCOPES` |
|
|
299
|
+
| `FLOAT` | `ALL_SCOPES`, `TEXT_CONTENT`, `CORNER_RADIUS`, `WIDTH_HEIGHT`, `GAP`, `OPACITY`, `STROKE_FLOAT`, `EFFECT_FLOAT`, `FONT_WEIGHT`, `FONT_SIZE`, `LINE_HEIGHT`, `LETTER_SPACING`, `PARAGRAPH_SPACING`, `PARAGRAPH_INDENT` |
|
|
300
|
+
| `STRING` | `ALL_SCOPES`, `TEXT_CONTENT`, `FONT_FAMILY`, `FONT_STYLE` |
|
|
301
|
+
| `COLOR` | `ALL_SCOPES`, `ALL_FILLS`, `FRAME_FILL`, `SHAPE_FILL`, `TEXT_FILL`, `STROKE_COLOR`, `EFFECT_COLOR` |
|
|
302
|
+
|
|
303
|
+
#### VariableCodeSyntax
|
|
304
|
+
|
|
305
|
+
Platform-specific code representations displayed in Dev Mode.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
interface VariableCodeSyntax {
|
|
309
|
+
WEB?: string; // e.g., "var(--color-primary)"
|
|
310
|
+
ANDROID?: string; // e.g., "@color/primary"
|
|
311
|
+
iOS?: string; // e.g., "Color.primary"
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Variable Resolution
|
|
318
|
+
|
|
319
|
+
#### Direct Values vs Aliases
|
|
320
|
+
|
|
321
|
+
A variable's value for each mode can be either a **direct value** (literal boolean, number, string, or color) or a **variable alias** (reference to another variable).
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
// Direct value example (COLOR type, mode "light"):
|
|
325
|
+
{
|
|
326
|
+
"valuesByMode": {
|
|
327
|
+
"mode-light-id": { "r": 0.067, "g": 0.067, "b": 0.067, "a": 1 },
|
|
328
|
+
"mode-dark-id": { "r": 0.933, "g": 0.933, "b": 0.933, "a": 1 }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Alias example (references another variable):
|
|
333
|
+
{
|
|
334
|
+
"valuesByMode": {
|
|
335
|
+
"mode-light-id": { "type": "VARIABLE_ALIAS", "id": "VariableID:1234:0" },
|
|
336
|
+
"mode-dark-id": { "type": "VARIABLE_ALIAS", "id": "VariableID:5678:0" }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### Resolving Alias Chains
|
|
342
|
+
|
|
343
|
+
Aliases can chain: Variable A references Variable B which references Variable C. To resolve to a final value, follow the chain:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
function resolveVariable(
|
|
347
|
+
variableId: string,
|
|
348
|
+
modeId: string,
|
|
349
|
+
variables: Record<string, Variable>
|
|
350
|
+
): VariableValue {
|
|
351
|
+
const variable = variables[variableId];
|
|
352
|
+
if (!variable) throw new Error(`Variable ${variableId} not found`);
|
|
353
|
+
|
|
354
|
+
const value = variable.valuesByMode[modeId];
|
|
355
|
+
if (!value) {
|
|
356
|
+
// Fall back to default mode
|
|
357
|
+
const collection = findCollection(variable.variableCollectionId);
|
|
358
|
+
return variable.valuesByMode[collection.defaultModeId];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// If it's an alias, resolve recursively
|
|
362
|
+
if (typeof value === 'object' && 'type' in value && value.type === 'VARIABLE_ALIAS') {
|
|
363
|
+
return resolveVariable(value.id, modeId, variables);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
> **Note:** Figma prevents alias cycles — a variable cannot reference itself directly or through a chain.
|
|
371
|
+
|
|
372
|
+
#### Mode Resolution and Fallbacks
|
|
373
|
+
|
|
374
|
+
Each variable has values indexed by mode ID in its `valuesByMode` map. Resolution order:
|
|
375
|
+
|
|
376
|
+
1. Look up the value for the **requested mode ID**
|
|
377
|
+
2. If no value exists for that mode, use the **collection's `defaultModeId`**
|
|
378
|
+
3. If the value is an alias, resolve the referenced variable using the **same mode ID**
|
|
379
|
+
|
|
380
|
+
When an alias references a variable in a **different** collection, the referenced variable resolves using its own collection's modes. Cross-collection mode mapping is not automatic.
|
|
381
|
+
|
|
382
|
+
#### Bound Variables on Nodes
|
|
383
|
+
|
|
384
|
+
Nodes in the Figma file tree indicate which variables are bound to their properties via the `boundVariables` field:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
// Example: a frame with variables bound to fills and corner radius
|
|
388
|
+
{
|
|
389
|
+
"type": "FRAME",
|
|
390
|
+
"name": "Card",
|
|
391
|
+
"boundVariables": {
|
|
392
|
+
"fills": [
|
|
393
|
+
{
|
|
394
|
+
"type": "VARIABLE_ALIAS",
|
|
395
|
+
"id": "VariableID:1234:0"
|
|
396
|
+
}
|
|
397
|
+
],
|
|
398
|
+
"topLeftRadius": {
|
|
399
|
+
"type": "VARIABLE_ALIAS",
|
|
400
|
+
"id": "VariableID:5678:0"
|
|
401
|
+
},
|
|
402
|
+
"topRightRadius": {
|
|
403
|
+
"type": "VARIABLE_ALIAS",
|
|
404
|
+
"id": "VariableID:5678:0"
|
|
405
|
+
},
|
|
406
|
+
"itemSpacing": {
|
|
407
|
+
"type": "VARIABLE_ALIAS",
|
|
408
|
+
"id": "VariableID:9012:0"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Properties that can have bound variables include:** fills, strokes, effects, cornerRadius (and individual corners), itemSpacing, paddingLeft/Right/Top/Bottom, width, height, opacity, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, and more.
|
|
415
|
+
|
|
416
|
+
The `boundVariables` map tells you which properties are token-driven vs hard-coded, which is essential for design-to-code token mapping.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
### Multi-Mode Theming
|
|
421
|
+
|
|
422
|
+
#### Pattern: Light/Dark Mode
|
|
423
|
+
|
|
424
|
+
The most common multi-mode pattern is light and dark themes.
|
|
425
|
+
|
|
426
|
+
**Figma structure:**
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
Collection: "Theme"
|
|
430
|
+
Modes: ["Light", "Dark"]
|
|
431
|
+
Variables:
|
|
432
|
+
"bg/primary" → Light: #FFFFFF, Dark: #1A1A1A
|
|
433
|
+
"bg/secondary" → Light: #F5F5F5, Dark: #2D2D2D
|
|
434
|
+
"text/primary" → Light: #1A1A1A, Dark: #FFFFFF
|
|
435
|
+
"text/secondary" → Light: #666666, Dark: #A0A0A0
|
|
436
|
+
"border/default" → Light: #E0E0E0, Dark: #404040
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**CSS output (class-based switching):**
|
|
440
|
+
|
|
441
|
+
```css
|
|
442
|
+
:root,
|
|
443
|
+
[data-theme="light"] {
|
|
444
|
+
--bg-primary: #ffffff;
|
|
445
|
+
--bg-secondary: #f5f5f5;
|
|
446
|
+
--text-primary: #1a1a1a;
|
|
447
|
+
--text-secondary: #666666;
|
|
448
|
+
--border-default: #e0e0e0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
[data-theme="dark"] {
|
|
452
|
+
--bg-primary: #1a1a1a;
|
|
453
|
+
--bg-secondary: #2d2d2d;
|
|
454
|
+
--text-primary: #ffffff;
|
|
455
|
+
--text-secondary: #a0a0a0;
|
|
456
|
+
--border-default: #404040;
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**CSS output (media query):**
|
|
461
|
+
|
|
462
|
+
```css
|
|
463
|
+
:root {
|
|
464
|
+
--bg-primary: #ffffff;
|
|
465
|
+
--text-primary: #1a1a1a;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
@media (prefers-color-scheme: dark) {
|
|
469
|
+
:root {
|
|
470
|
+
--bg-primary: #1a1a1a;
|
|
471
|
+
--text-primary: #ffffff;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Pattern: Brand Theming
|
|
477
|
+
|
|
478
|
+
Multiple brands sharing a common token structure.
|
|
479
|
+
|
|
480
|
+
**Figma structure:**
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
Collection: "Brand"
|
|
484
|
+
Modes: ["Brand A", "Brand B", "Brand C"]
|
|
485
|
+
Variables:
|
|
486
|
+
"color/primary" → A: #0066FF, B: #FF3366, C: #00CC88
|
|
487
|
+
"color/secondary" → A: #003399, B: #CC0033, C: #009966
|
|
488
|
+
"font/heading" → A: "Inter", B: "Poppins", C: "DM Sans"
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**CSS output (class-based):**
|
|
492
|
+
|
|
493
|
+
```css
|
|
494
|
+
.brand-a {
|
|
495
|
+
--color-primary: #0066ff;
|
|
496
|
+
--color-secondary: #003399;
|
|
497
|
+
--font-heading: 'Inter', sans-serif;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.brand-b {
|
|
501
|
+
--color-primary: #ff3366;
|
|
502
|
+
--color-secondary: #cc0033;
|
|
503
|
+
--font-heading: 'Poppins', sans-serif;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.brand-c {
|
|
507
|
+
--color-primary: #00cc88;
|
|
508
|
+
--color-secondary: #009966;
|
|
509
|
+
--font-heading: 'DM Sans', sans-serif;
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### Pattern: Layered Modes (Theme + Density)
|
|
514
|
+
|
|
515
|
+
Multiple collections with independent modes that compose together.
|
|
516
|
+
|
|
517
|
+
**Figma structure:**
|
|
518
|
+
|
|
519
|
+
```
|
|
520
|
+
Collection: "Theme"
|
|
521
|
+
Modes: ["Light", "Dark"]
|
|
522
|
+
Variables: color tokens...
|
|
523
|
+
|
|
524
|
+
Collection: "Density"
|
|
525
|
+
Modes: ["Compact", "Default", "Comfortable"]
|
|
526
|
+
Variables: spacing tokens...
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
In CSS, these compose naturally because each collection maps to independent custom properties:
|
|
530
|
+
|
|
531
|
+
```css
|
|
532
|
+
/* Theme layer */
|
|
533
|
+
[data-theme="light"] { --bg: #fff; --text: #1a1a1a; }
|
|
534
|
+
[data-theme="dark"] { --bg: #1a1a1a; --text: #fff; }
|
|
535
|
+
|
|
536
|
+
/* Density layer (independent) */
|
|
537
|
+
[data-density="compact"] { --spacing-md: 8px; --spacing-lg: 12px; }
|
|
538
|
+
[data-density="default"] { --spacing-md: 16px; --spacing-lg: 24px; }
|
|
539
|
+
[data-density="comfortable"] { --spacing-md: 24px; --spacing-lg: 32px; }
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Practical Patterns
|
|
545
|
+
|
|
546
|
+
#### Extracting All Color Tokens
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
interface ColorToken {
|
|
550
|
+
name: string;
|
|
551
|
+
variableId: string;
|
|
552
|
+
collection: string;
|
|
553
|
+
values: Record<string, { hex: string; rgba: RGBA }>;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function extractColorTokens(fileKey: string): Promise<ColorToken[]> {
|
|
557
|
+
const res = await fetch(
|
|
558
|
+
`https://api.figma.com/v1/files/${fileKey}/variables/local`,
|
|
559
|
+
{ headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
|
|
560
|
+
);
|
|
561
|
+
const { meta } = await res.json();
|
|
562
|
+
const tokens: ColorToken[] = [];
|
|
563
|
+
|
|
564
|
+
for (const variable of Object.values(meta.variables) as Variable[]) {
|
|
565
|
+
if (variable.resolvedType !== 'COLOR') continue;
|
|
566
|
+
if (variable.remote) continue; // Skip imported variables
|
|
567
|
+
|
|
568
|
+
const collection = meta.variableCollections[variable.variableCollectionId];
|
|
569
|
+
|
|
570
|
+
const values: Record<string, { hex: string; rgba: RGBA }> = {};
|
|
571
|
+
for (const [modeId, value] of Object.entries(variable.valuesByMode)) {
|
|
572
|
+
const resolved = resolveToColor(value as VariableValue, modeId, meta.variables);
|
|
573
|
+
if (resolved) {
|
|
574
|
+
const modeName = collection.modes[modeId] || modeId;
|
|
575
|
+
values[modeName] = {
|
|
576
|
+
hex: rgbaToHex(resolved),
|
|
577
|
+
rgba: resolved,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
tokens.push({
|
|
583
|
+
name: variable.name,
|
|
584
|
+
variableId: variable.id,
|
|
585
|
+
collection: collection.name,
|
|
586
|
+
values,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return tokens;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function rgbaToHex(c: RGBA): string {
|
|
594
|
+
const toHex = (v: number) => Math.round(v * 255).toString(16).padStart(2, '0');
|
|
595
|
+
const hex = `#${toHex(c.r)}${toHex(c.g)}${toHex(c.b)}`;
|
|
596
|
+
return c.a < 1 ? `${hex}${toHex(c.a)}` : hex;
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### Building a Complete Token Dictionary
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
interface TokenDictionary {
|
|
604
|
+
colors: Record<string, Record<string, string>>; // name → { modeName: hex }
|
|
605
|
+
numbers: Record<string, Record<string, number>>; // name → { modeName: value }
|
|
606
|
+
strings: Record<string, Record<string, string>>; // name → { modeName: value }
|
|
607
|
+
booleans: Record<string, Record<string, boolean>>; // name → { modeName: value }
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function buildTokenDictionary(fileKey: string): Promise<TokenDictionary> {
|
|
611
|
+
const res = await fetch(
|
|
612
|
+
`https://api.figma.com/v1/files/${fileKey}/variables/local`,
|
|
613
|
+
{ headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
|
|
614
|
+
);
|
|
615
|
+
const { meta } = await res.json();
|
|
616
|
+
|
|
617
|
+
const dict: TokenDictionary = { colors: {}, numbers: {}, strings: {}, booleans: {} };
|
|
618
|
+
|
|
619
|
+
for (const variable of Object.values(meta.variables) as Variable[]) {
|
|
620
|
+
if (variable.remote) continue;
|
|
621
|
+
if (variable.hiddenFromPublishing) continue;
|
|
622
|
+
|
|
623
|
+
const collection = meta.variableCollections[variable.variableCollectionId];
|
|
624
|
+
const tokenName = `${collection.name}/${variable.name}`;
|
|
625
|
+
|
|
626
|
+
for (const [modeId, rawValue] of Object.entries(variable.valuesByMode)) {
|
|
627
|
+
const modeName = collection.modes[modeId] || modeId;
|
|
628
|
+
const value = resolveValue(rawValue as VariableValue, modeId, meta.variables);
|
|
629
|
+
|
|
630
|
+
switch (variable.resolvedType) {
|
|
631
|
+
case 'COLOR':
|
|
632
|
+
dict.colors[tokenName] ??= {};
|
|
633
|
+
dict.colors[tokenName][modeName] = rgbaToHex(value as RGBA);
|
|
634
|
+
break;
|
|
635
|
+
case 'FLOAT':
|
|
636
|
+
dict.numbers[tokenName] ??= {};
|
|
637
|
+
dict.numbers[tokenName][modeName] = value as number;
|
|
638
|
+
break;
|
|
639
|
+
case 'STRING':
|
|
640
|
+
dict.strings[tokenName] ??= {};
|
|
641
|
+
dict.strings[tokenName][modeName] = value as string;
|
|
642
|
+
break;
|
|
643
|
+
case 'BOOLEAN':
|
|
644
|
+
dict.booleans[tokenName] ??= {};
|
|
645
|
+
dict.booleans[tokenName][modeName] = value as boolean;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return dict;
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### Detecting Which Variables Are Actually Used
|
|
656
|
+
|
|
657
|
+
Combine file tree traversal with the Variables API to find which tokens are actively bound to design elements:
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
async function findUsedVariables(fileKey: string): Promise<Set<string>> {
|
|
661
|
+
// 1. Fetch the file tree
|
|
662
|
+
const fileRes = await fetch(
|
|
663
|
+
`https://api.figma.com/v1/files/${fileKey}`,
|
|
664
|
+
{ headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
|
|
665
|
+
);
|
|
666
|
+
const file = await fileRes.json();
|
|
667
|
+
|
|
668
|
+
// 2. Walk the tree and collect all bound variable IDs
|
|
669
|
+
const usedIds = new Set<string>();
|
|
670
|
+
|
|
671
|
+
function walk(node: any) {
|
|
672
|
+
if (node.boundVariables) {
|
|
673
|
+
for (const bindings of Object.values(node.boundVariables)) {
|
|
674
|
+
if (Array.isArray(bindings)) {
|
|
675
|
+
for (const b of bindings) {
|
|
676
|
+
if (b && typeof b === 'object' && b.type === 'VARIABLE_ALIAS') {
|
|
677
|
+
usedIds.add(b.id);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} else if (bindings && typeof bindings === 'object') {
|
|
681
|
+
const b = bindings as any;
|
|
682
|
+
if (b.type === 'VARIABLE_ALIAS') {
|
|
683
|
+
usedIds.add(b.id);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (node.children) {
|
|
689
|
+
node.children.forEach(walk);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
walk(file.document);
|
|
694
|
+
return usedIds;
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
#### Generating CSS Custom Properties from Variables
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
async function generateCSSVariables(fileKey: string): Promise<string> {
|
|
702
|
+
const res = await fetch(
|
|
703
|
+
`https://api.figma.com/v1/files/${fileKey}/variables/local`,
|
|
704
|
+
{ headers: { 'X-Figma-Token': process.env.FIGMA_TOKEN! } }
|
|
705
|
+
);
|
|
706
|
+
const { meta } = await res.json();
|
|
707
|
+
|
|
708
|
+
const output: string[] = [];
|
|
709
|
+
|
|
710
|
+
// Group variables by collection
|
|
711
|
+
for (const collection of Object.values(meta.variableCollections) as VariableCollection[]) {
|
|
712
|
+
if (collection.remote) continue;
|
|
713
|
+
|
|
714
|
+
const collectionVars = collection.variableIds
|
|
715
|
+
.map(id => meta.variables[id])
|
|
716
|
+
.filter((v): v is Variable => v != null && !v.hiddenFromPublishing);
|
|
717
|
+
|
|
718
|
+
// Generate CSS for each mode
|
|
719
|
+
const modeEntries = Object.entries(collection.modes);
|
|
720
|
+
|
|
721
|
+
for (const [modeId, modeName] of modeEntries) {
|
|
722
|
+
const isDefault = modeId === collection.defaultModeId;
|
|
723
|
+
const selector = isDefault
|
|
724
|
+
? ':root'
|
|
725
|
+
: `[data-${slugify(collection.name)}="${slugify(modeName)}"]`;
|
|
726
|
+
|
|
727
|
+
output.push(`${selector} {`);
|
|
728
|
+
|
|
729
|
+
for (const variable of collectionVars) {
|
|
730
|
+
const value = variable.valuesByMode[modeId];
|
|
731
|
+
if (!value) continue;
|
|
732
|
+
|
|
733
|
+
const resolved = resolveValue(value, modeId, meta.variables);
|
|
734
|
+
const cssName = `--${tokenToCSSName(variable.name)}`;
|
|
735
|
+
const cssValue = formatCSSValue(resolved, variable.resolvedType);
|
|
736
|
+
|
|
737
|
+
output.push(` ${cssName}: ${cssValue};`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
output.push('}');
|
|
741
|
+
output.push('');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return output.join('\n');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function tokenToCSSName(name: string): string {
|
|
749
|
+
return name
|
|
750
|
+
.replace(/\//g, '-')
|
|
751
|
+
.replace(/\s+/g, '-')
|
|
752
|
+
.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
|
|
753
|
+
.replace(/^-/, '')
|
|
754
|
+
.toLowerCase();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function slugify(str: string): string {
|
|
758
|
+
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function formatCSSValue(value: any, type: string): string {
|
|
762
|
+
switch (type) {
|
|
763
|
+
case 'COLOR': {
|
|
764
|
+
const c = value as RGBA;
|
|
765
|
+
if (c.a < 1) {
|
|
766
|
+
return `rgba(${Math.round(c.r * 255)}, ${Math.round(c.g * 255)}, ${Math.round(c.b * 255)}, ${c.a})`;
|
|
767
|
+
}
|
|
768
|
+
return rgbaToHex(c);
|
|
769
|
+
}
|
|
770
|
+
case 'FLOAT':
|
|
771
|
+
return `${value}px`;
|
|
772
|
+
case 'STRING':
|
|
773
|
+
return `'${value}'`;
|
|
774
|
+
case 'BOOLEAN':
|
|
775
|
+
return value ? '1' : '0';
|
|
776
|
+
default:
|
|
777
|
+
return String(value);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
#### Platform-Agnostic Token Export
|
|
783
|
+
|
|
784
|
+
The `codeSyntax` field on variables provides platform-specific token names for Dev Mode:
|
|
785
|
+
|
|
786
|
+
```ts
|
|
787
|
+
// Example: use codeSyntax for platform-aware export
|
|
788
|
+
function exportForPlatform(
|
|
789
|
+
variable: Variable,
|
|
790
|
+
platform: 'WEB' | 'ANDROID' | 'iOS'
|
|
791
|
+
): string | null {
|
|
792
|
+
// Prefer explicit codeSyntax if set by designers
|
|
793
|
+
if (variable.codeSyntax[platform]) {
|
|
794
|
+
return variable.codeSyntax[platform];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Fall back to auto-generated name
|
|
798
|
+
switch (platform) {
|
|
799
|
+
case 'WEB':
|
|
800
|
+
return `var(--${tokenToCSSName(variable.name)})`;
|
|
801
|
+
case 'ANDROID':
|
|
802
|
+
return `@dimen/${variable.name.replace(/\//g, '_').toLowerCase()}`;
|
|
803
|
+
case 'iOS':
|
|
804
|
+
return variable.name.replace(/\//g, '.').replace(/\s+/g, '');
|
|
805
|
+
default:
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
### Token Extraction Priority
|
|
814
|
+
|
|
815
|
+
When extracting design tokens from a Figma file, use this priority order:
|
|
816
|
+
|
|
817
|
+
1. **Variables API** (this module) — Best structured source. Includes modes, scopes, aliases, and explicit token definitions. Requires Enterprise plan.
|
|
818
|
+
|
|
819
|
+
2. **Published styles** — Use `/files/:key/styles` for published library styles. Limited to published content only. See `figma-api-rest.md`.
|
|
820
|
+
|
|
821
|
+
3. **File tree traversal + inference** — Walk the node tree, read fill/stroke/effect properties, and derive tokens by frequency analysis (threshold-based promotion). Works on all plans but requires heuristics.
|
|
822
|
+
|
|
823
|
+
4. **Plugin API** — For local styles and variables when REST is plan-limited. Requires running a Figma plugin in-context. See `figma-api-plugin.md`.
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
### Error Handling
|
|
828
|
+
|
|
829
|
+
| Status | Meaning | Action |
|
|
830
|
+
|--------|---------|--------|
|
|
831
|
+
| `400` | Invalid parameter (bad variable name, scope mismatch) | Check request body against constraints |
|
|
832
|
+
| `401` | Authentication failed | Verify token and `file_variables:read/write` scope |
|
|
833
|
+
| `403` | Insufficient permissions (non-Enterprise or guest) | Confirm Enterprise plan, full member status, and file access |
|
|
834
|
+
| `404` | File not found | Verify file key |
|
|
835
|
+
| `413` | Payload exceeds 4 MB | Split POST request into smaller batches |
|
|
836
|
+
| `429` | Rate limited | Use `Retry-After` header. See `figma-api-rest.md` for backoff strategy |
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## Cross-References
|
|
841
|
+
|
|
842
|
+
- **`figma-api-rest.md`** — Core REST API reference (authentication, file endpoints, rate limits, node structure)
|
|
843
|
+
- **`figma-api-plugin.md`** — Plugin API alternative for variables access on non-Enterprise plans
|
|
844
|
+
- **`figma-api-webhooks.md`** — Webhooks v2 LIBRARY_PUBLISH event for reacting to published variable changes
|
|
845
|
+
- **`design-tokens.md`** — Token extraction strategies, CSS variable naming, threshold-based promotion
|
|
846
|
+
- **`design-tokens-variables.md`** — Deep dive on Figma Variables patterns (modes, fallback chains, scope usage)
|
|
847
|
+
- **`css-strategy.md`** — Layered CSS approach for consuming design tokens
|
|
848
|
+
- **`design-to-code-visual.md`** — Visual property extraction that consumes bound variable data
|