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,1261 @@
|
|
|
1
|
+
# Figma Variables → Design Token → CSS Bridge
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Authoritative reference for the bridge between Figma Variables and CSS Custom Properties in a design token system. Documents how to extract variable collections, detect and classify their modes (breakpoint vs theme), resolve variable values through alias chains, generate mode-aware CSS output with media queries and theme selectors, and handle fallback values for external library variables. Encodes production patterns for variable collection processing, mode classification, CSS rendering, and token lookup.
|
|
6
|
+
|
|
7
|
+
This module fills the gap between `figma-api-variables.md` (API-level reference for Variables endpoints and data model) and `design-tokens.md` (general token extraction pipeline). It focuses specifically on the Variables-to-Token-to-CSS transformation that neither of those modules covers in depth.
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
Reference this module when you need to:
|
|
12
|
+
|
|
13
|
+
- Understand how Figma Variable collections map to token categories for CSS output
|
|
14
|
+
- Detect whether a collection's modes represent breakpoints (responsive) or themes (light/dark)
|
|
15
|
+
- Determine which mode is the "default" (base) mode for mobile-first or light-default rendering
|
|
16
|
+
- Resolve variable alias chains to terminal values for each mode
|
|
17
|
+
- Extract bound variable references from node properties (fills, strokes, layout, text)
|
|
18
|
+
- Convert Figma variable paths to CSS custom property names
|
|
19
|
+
- Generate mode-aware CSS: `:root` for base, `@media (min-width)` for breakpoints, `prefers-color-scheme` and `[data-theme]` for themes
|
|
20
|
+
- Decide when to include fallback values in `var()` references (local vs external variables)
|
|
21
|
+
- Map variable scopes to applicable CSS properties
|
|
22
|
+
- Design multi-mode token systems (responsive + theme cross-products)
|
|
23
|
+
- Understand how variable tokens integrate with and take priority over auto-detected tokens
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Content
|
|
28
|
+
|
|
29
|
+
### 1. Purpose & When to Use
|
|
30
|
+
|
|
31
|
+
Figma Variables are the highest-priority source for design tokens. They provide structured, designer-curated token definitions with explicit names, types, scopes, and per-mode values -- far superior to heuristic-based token extraction from file traversal.
|
|
32
|
+
|
|
33
|
+
#### When This Module Applies
|
|
34
|
+
|
|
35
|
+
- **The Figma file uses Variables** for colors, spacing, typography, or other design values
|
|
36
|
+
- **The file has multi-mode collections** (responsive breakpoints, light/dark themes, brand variants)
|
|
37
|
+
- **You need CSS Custom Properties** that adapt to viewport size or theme preference
|
|
38
|
+
- **You need to resolve variable bindings** from node properties to CSS `var()` references
|
|
39
|
+
- **You are building a token pipeline** that bridges Figma Variables to a CSS/SCSS/Tailwind output
|
|
40
|
+
|
|
41
|
+
#### When to Use Other Modules Instead
|
|
42
|
+
|
|
43
|
+
| Scenario | Module |
|
|
44
|
+
|----------|--------|
|
|
45
|
+
| Variables API endpoints, authentication, data model | `figma-api-variables.md` |
|
|
46
|
+
| General token extraction (collect, promote, name, render) | `design-tokens.md` |
|
|
47
|
+
| Where tokens go in the CSS output (Layer 1/2/3) | `css-strategy.md` |
|
|
48
|
+
| Visual property extraction (fills, strokes, effects) | `design-to-code-visual.md` |
|
|
49
|
+
| Layout property extraction (gap, padding) | `design-to-code-layout.md` |
|
|
50
|
+
| Typography property extraction (fonts, sizes, weights) | `design-to-code-typography.md` |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### 2. Variables as Token Source
|
|
55
|
+
|
|
56
|
+
Variables are the preferred token source because they provide structured, explicit token definitions -- names, types, modes, scopes, and alias relationships -- rather than requiring heuristic detection from raw property values.
|
|
57
|
+
|
|
58
|
+
#### Token Source Priority
|
|
59
|
+
|
|
60
|
+
The token extraction pipeline uses this priority order (documented in `figma-api-variables.md`):
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
1. Figma Variables API → Best: structured, multi-mode, scoped
|
|
64
|
+
2. Published styles → Good: limited to published content
|
|
65
|
+
3. File tree traversal → Fallback: heuristic-based, all plans
|
|
66
|
+
4. Plugin API (in-context) → Fallback: for non-Enterprise REST access
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Variables provide everything the token system needs without heuristics:
|
|
70
|
+
|
|
71
|
+
| Feature | Variables API | File Traversal |
|
|
72
|
+
|---------|:---:|:---:|
|
|
73
|
+
| Explicit token names | Yes (`color/primary/500`) | No (inferred from HSL) |
|
|
74
|
+
| Multi-mode values | Yes (per breakpoint/theme) | No (single value per node) |
|
|
75
|
+
| Type safety | Yes (`COLOR`, `FLOAT`, `STRING`) | Inferred from context |
|
|
76
|
+
| Scope constraints | Yes (`ALL_FILLS`, `GAP`, etc.) | No |
|
|
77
|
+
| Alias chains | Yes (variable referencing variable) | No |
|
|
78
|
+
| Designer intent | Explicit | Inferred |
|
|
79
|
+
|
|
80
|
+
#### When Variables API Is Not Available
|
|
81
|
+
|
|
82
|
+
The Variables REST API requires Enterprise plan + full member access. When this is not available:
|
|
83
|
+
|
|
84
|
+
1. **Plugin API** -- Access local variables via `figma.variables.getLocalVariableCollectionsAsync()` and `figma.variables.getVariableByIdAsync()`. This is the recommended approach for Figma plugins (running inside the Figma sandbox, not as a REST client).
|
|
85
|
+
|
|
86
|
+
2. **Bound variable extraction** -- Even without the Variables API, nodes in the file tree expose `boundVariables` that reference variable IDs. The file tree response includes these bindings.
|
|
87
|
+
|
|
88
|
+
3. **File traversal + promotion** -- Fall back to the threshold-based promotion system documented in `design-tokens.md`. Traverse the node tree, collect raw values, and promote repeated values to tokens.
|
|
89
|
+
|
|
90
|
+
#### How Variables and Auto-Detected Tokens Coexist
|
|
91
|
+
|
|
92
|
+
Variables and auto-detected tokens are collected in parallel and merged. Variables always win conflicts:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
collectAllTokens(node) →
|
|
96
|
+
├── collectColors(node) → ColorToken[] (auto-detected)
|
|
97
|
+
├── collectSpacing(node) → SpacingToken[] (auto-detected)
|
|
98
|
+
├── collectTypography(node) → TypographyTokens (auto-detected)
|
|
99
|
+
├── collectEffects(node) → EffectTokens (auto-detected)
|
|
100
|
+
└── collectFigmaVariables(node) → FigmaVariableToken[] (explicit bindings)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
During the lookup phase, the priority chain is:
|
|
104
|
+
|
|
105
|
+
1. **Bound variable** on the property -- use `var(--css-name)` directly
|
|
106
|
+
2. **Auto-detected token** from the lookup map -- use `var(--token-name)`
|
|
107
|
+
3. **Raw value** -- use the literal CSS value
|
|
108
|
+
|
|
109
|
+
> See `design-tokens.md` Section 9 for the complete token lookup system.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 3. Collection Structure for Tokens
|
|
114
|
+
|
|
115
|
+
Figma variable collections group related variables that share the same set of modes. Collections map naturally to token categories.
|
|
116
|
+
|
|
117
|
+
#### Collection → Token Category Mapping
|
|
118
|
+
|
|
119
|
+
Designers typically organize collections by token category:
|
|
120
|
+
|
|
121
|
+
| Collection Name | Token Category | Variable `resolvedType` | CSS Output |
|
|
122
|
+
|----------------|---------------|:-----------------------:|------------|
|
|
123
|
+
| `Colors` / `Theme` | Color tokens | `COLOR` | `--color-primary: #1a73e8` |
|
|
124
|
+
| `Spacing` | Spacing tokens | `FLOAT` | `--spacing-md: 16px` |
|
|
125
|
+
| `Typography` | Font tokens | `FLOAT` / `STRING` | `--text-lg: 18px`, `--font-primary: 'Inter'` |
|
|
126
|
+
| `Radii` | Border radius tokens | `FLOAT` | `--radius-md: 8px` |
|
|
127
|
+
| `Effects` | Shadow/elevation tokens | `FLOAT` | `--shadow-blur: 8px` |
|
|
128
|
+
|
|
129
|
+
#### Collection Naming Conventions
|
|
130
|
+
|
|
131
|
+
The pipeline does not enforce collection naming conventions -- it processes all local collections regardless of name. The collection name is used for:
|
|
132
|
+
|
|
133
|
+
1. **CSS comment headers** in the rendered output (e.g., `/* Colors */`)
|
|
134
|
+
2. **Mode type detection** -- mode names within the collection determine breakpoint vs theme rendering
|
|
135
|
+
3. **Grouping** -- variables from the same collection are rendered together in the CSS output
|
|
136
|
+
|
|
137
|
+
#### Multi-Collection Files
|
|
138
|
+
|
|
139
|
+
A single Figma file can have multiple collections, each with its own modes:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
File: "Design System"
|
|
143
|
+
Collection: "Theme"
|
|
144
|
+
Modes: [Light, Dark]
|
|
145
|
+
Variables: color/primary, color/bg, color/text, ...
|
|
146
|
+
|
|
147
|
+
Collection: "Spacing"
|
|
148
|
+
Modes: [Mobile, Tablet, Desktop]
|
|
149
|
+
Variables: spacing/sm, spacing/md, spacing/lg, ...
|
|
150
|
+
|
|
151
|
+
Collection: "Typography"
|
|
152
|
+
Modes: [Mobile, Desktop]
|
|
153
|
+
Variables: text/body, text/heading, ...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Each collection is rendered independently with its own mode-aware CSS blocks. Collections with a single mode render only in the base `:root` block.
|
|
157
|
+
|
|
158
|
+
#### Collection Processing
|
|
159
|
+
|
|
160
|
+
The recommended collection processing approach:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync()
|
|
164
|
+
return collections.map(collection => {
|
|
165
|
+
const modes = collection.modes.map(m => ({ id: m.modeId, name: m.name }))
|
|
166
|
+
const modeType = detectModeType(modes.map(m => m.name))
|
|
167
|
+
return {
|
|
168
|
+
id: collection.id,
|
|
169
|
+
name: collection.name,
|
|
170
|
+
modeType, // 'breakpoint' | 'theme' | 'unknown'
|
|
171
|
+
modes,
|
|
172
|
+
defaultModeId: getDefaultModeId(modes, modeType),
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Key points:
|
|
178
|
+
- Only **local** collections are processed (external library collections are skipped)
|
|
179
|
+
- Each collection gets a `modeType` classification (see Section 4)
|
|
180
|
+
- Each collection gets a `defaultModeId` identifying which mode provides base values
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 4. Mode Detection & Classification
|
|
185
|
+
|
|
186
|
+
Variable collections can have multiple modes. The token system must classify each collection's modes to determine how they map to CSS constructs (media queries vs theme selectors).
|
|
187
|
+
|
|
188
|
+
#### Mode Type Classification
|
|
189
|
+
|
|
190
|
+
The pipeline classifies collections into three types:
|
|
191
|
+
|
|
192
|
+
| Mode Type | Detection | CSS Rendering |
|
|
193
|
+
|-----------|-----------|---------------|
|
|
194
|
+
| `breakpoint` | Mode names contain "mobile", "tablet", "desktop", or pixel values | `@media (min-width)` queries |
|
|
195
|
+
| `theme` | Mode names contain "light", "dark", or "theme" | `prefers-color-scheme` + `[data-theme]` selectors |
|
|
196
|
+
| `unknown` | Neither pattern detected | Single mode in `:root` only |
|
|
197
|
+
|
|
198
|
+
#### Detection Algorithm
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
function detectModeType(modeNames: string[]): 'breakpoint' | 'theme' | 'unknown' {
|
|
202
|
+
const normalized = modeNames.map(n => n.toLowerCase().trim())
|
|
203
|
+
|
|
204
|
+
// Check theme patterns FIRST
|
|
205
|
+
const hasThemePatterns = normalized.some(name =>
|
|
206
|
+
name.includes('light') ||
|
|
207
|
+
name.includes('dark') ||
|
|
208
|
+
name.includes('theme')
|
|
209
|
+
)
|
|
210
|
+
if (hasThemePatterns) return 'theme'
|
|
211
|
+
|
|
212
|
+
// Check breakpoint patterns
|
|
213
|
+
const hasBreakpointPatterns = normalized.some(name =>
|
|
214
|
+
name.includes('desktop') ||
|
|
215
|
+
name.includes('mobile') ||
|
|
216
|
+
name.includes('tablet') ||
|
|
217
|
+
/^\d+px?$/.test(name) // "800px" or "800"
|
|
218
|
+
)
|
|
219
|
+
if (hasBreakpointPatterns) return 'breakpoint'
|
|
220
|
+
|
|
221
|
+
return 'unknown'
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Important ordering:** Theme detection runs before breakpoint detection. If a collection has modes named "Light Desktop" and "Dark Desktop", the "light"/"dark" patterns match first, classifying it as a theme collection.
|
|
226
|
+
|
|
227
|
+
#### Breakpoint Mode Detection Patterns
|
|
228
|
+
|
|
229
|
+
| Mode Name | Detected As | Breakpoint Value |
|
|
230
|
+
|-----------|:-----------:|:----------------:|
|
|
231
|
+
| `Mobile` | breakpoint | 0 (base) |
|
|
232
|
+
| `Tablet` | breakpoint | 768px |
|
|
233
|
+
| `Desktop` | breakpoint | 1024px |
|
|
234
|
+
| `800` | breakpoint | 800px |
|
|
235
|
+
| `800px` | breakpoint | 800px |
|
|
236
|
+
| `1440px` | breakpoint | 1440px |
|
|
237
|
+
|
|
238
|
+
Pixel-value mode names (e.g., `"800px"` or `"800"`) are parsed directly:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
function parseBreakpointValue(modeName: string): number | null {
|
|
242
|
+
const normalized = modeName.toLowerCase().trim()
|
|
243
|
+
|
|
244
|
+
// Standard names
|
|
245
|
+
const BREAKPOINT_MAP = { 'mobile': 0, 'tablet': 768, 'desktop': 1024 }
|
|
246
|
+
if (BREAKPOINT_MAP[normalized] !== undefined) {
|
|
247
|
+
return BREAKPOINT_MAP[normalized]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Pixel values: "800px" or "800"
|
|
251
|
+
const pxMatch = normalized.match(/^(\d+)px?$/)
|
|
252
|
+
if (pxMatch) return parseInt(pxMatch[1], 10)
|
|
253
|
+
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### Theme Mode Detection Patterns
|
|
259
|
+
|
|
260
|
+
| Mode Name | Detected As | Default? |
|
|
261
|
+
|-----------|:-----------:|:--------:|
|
|
262
|
+
| `Light` | theme | Yes (default) |
|
|
263
|
+
| `Dark` | theme | No |
|
|
264
|
+
| `Light Theme` | theme | Yes |
|
|
265
|
+
| `Dark Mode` | theme | No |
|
|
266
|
+
|
|
267
|
+
#### Default Mode Identification
|
|
268
|
+
|
|
269
|
+
The "default" mode is the one that renders in the base `:root` block (no media query, no theme selector). Non-default modes render in conditional blocks.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
function getDefaultModeId(modes, modeType): string {
|
|
273
|
+
if (modeType === 'breakpoint') {
|
|
274
|
+
// Mobile-first: smallest breakpoint value is default
|
|
275
|
+
let minValue = Infinity
|
|
276
|
+
for (const mode of modes) {
|
|
277
|
+
const value = parseBreakpointValue(mode.name)
|
|
278
|
+
if (value !== null && value < minValue) {
|
|
279
|
+
minValue = value
|
|
280
|
+
defaultId = mode.id
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return defaultId
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (modeType === 'theme') {
|
|
287
|
+
// "Light" is default
|
|
288
|
+
const lightMode = modes.find(m => m.name.toLowerCase().includes('light'))
|
|
289
|
+
if (lightMode) return lightMode.id
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Unknown: first mode is default
|
|
293
|
+
return modes[0].id
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| Mode Type | Default Rule | Rationale |
|
|
298
|
+
|-----------|-------------|-----------|
|
|
299
|
+
| Breakpoint | Smallest value (mobile = 0) | Mobile-first CSS: base styles have no media query |
|
|
300
|
+
| Theme | Mode containing "light" | Light theme is the conventional default |
|
|
301
|
+
| Unknown | First mode in array | Figma's default mode is typically first |
|
|
302
|
+
|
|
303
|
+
#### Breakpoint Sort Order (Mobile-First)
|
|
304
|
+
|
|
305
|
+
Breakpoint modes are sorted ascending by pixel value for CSS output. The default mode (mobile, value 0) comes first with no media query. Larger breakpoints follow with `min-width` queries:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
function sortBreakpointModes(modes, defaultModeId) {
|
|
309
|
+
return modes
|
|
310
|
+
.map(mode => ({
|
|
311
|
+
...mode,
|
|
312
|
+
breakpoint: mode.id === defaultModeId ? null : parseBreakpointValue(mode.name)
|
|
313
|
+
}))
|
|
314
|
+
.sort((a, b) => {
|
|
315
|
+
if (a.breakpoint === null) return -1 // Default first
|
|
316
|
+
if (b.breakpoint === null) return 1
|
|
317
|
+
return (a.breakpoint || 0) - (b.breakpoint || 0) // Ascending
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Result: `[mobile (null), tablet (768), desktop (1024)]`
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### 5. Variable Resolution Chain
|
|
327
|
+
|
|
328
|
+
The complete resolution from a Figma variable binding to a CSS custom property value follows this chain:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
Node property has bound variable
|
|
332
|
+
↓
|
|
333
|
+
Look up Variable by ID
|
|
334
|
+
↓
|
|
335
|
+
Check valuesByMode for requested mode
|
|
336
|
+
↓
|
|
337
|
+
Is value a VARIABLE_ALIAS?
|
|
338
|
+
├─ YES → Resolve referenced variable (recursively)
|
|
339
|
+
└─ NO → Terminal value (COLOR, FLOAT, STRING, BOOLEAN)
|
|
340
|
+
↓
|
|
341
|
+
Format terminal value as CSS string
|
|
342
|
+
↓
|
|
343
|
+
Generate CSS custom property: --name: value
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### Alias Resolution
|
|
347
|
+
|
|
348
|
+
Variables can reference other variables (aliases). An alias is detected by the `VARIABLE_ALIAS` type:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
if (typeof modeValue === 'object' && modeValue !== null &&
|
|
352
|
+
'type' in modeValue && modeValue.type === 'VARIABLE_ALIAS') {
|
|
353
|
+
// Resolve the aliased variable
|
|
354
|
+
const aliasedVar = await figma.variables.getVariableByIdAsync(modeValue.id)
|
|
355
|
+
if (aliasedVar) {
|
|
356
|
+
resolvedValue = aliasedVar.valuesByMode[modeId]
|
|
357
|
+
?? aliasedVar.valuesByMode[collection.defaultModeId]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Key behaviors:
|
|
363
|
+
- Aliases can chain: Variable A references Variable B which references Variable C
|
|
364
|
+
- Cross-collection aliases are valid: a color variable in "Theme" can reference a variable in "Primitives"
|
|
365
|
+
- When a referenced variable has no value for the requested mode, the collection's default mode value is used
|
|
366
|
+
- Figma prevents alias cycles (no self-referencing)
|
|
367
|
+
|
|
368
|
+
> For the full alias resolution algorithm and API details, see `figma-api-variables.md` Section "Variable Resolution".
|
|
369
|
+
|
|
370
|
+
#### Terminal Value Types
|
|
371
|
+
|
|
372
|
+
After resolving all aliases, the terminal value is one of four types:
|
|
373
|
+
|
|
374
|
+
| `resolvedType` | TypeScript Type | CSS Formatting |
|
|
375
|
+
|----------------|----------------|----------------|
|
|
376
|
+
| `COLOR` | `{ r, g, b, a }` (0-1 floats) | `#RRGGBB` or `rgba(R, G, B, A)` |
|
|
377
|
+
| `FLOAT` | `number` | `{value}px` |
|
|
378
|
+
| `STRING` | `string` | `'{value}'` (quoted for fonts) |
|
|
379
|
+
| `BOOLEAN` | `boolean` | `1` or `0` |
|
|
380
|
+
|
|
381
|
+
#### Value Formatting
|
|
382
|
+
|
|
383
|
+
From `variables.ts`:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
function formatVariableValue(value, type): string {
|
|
387
|
+
if (type === 'COLOR' && typeof value === 'object' && 'r' in value) {
|
|
388
|
+
const r = Math.round(value.r * 255)
|
|
389
|
+
const g = Math.round(value.g * 255)
|
|
390
|
+
const b = Math.round(value.b * 255)
|
|
391
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase()
|
|
392
|
+
}
|
|
393
|
+
if (type === 'FLOAT' && typeof value === 'number') {
|
|
394
|
+
return `${value}px`
|
|
395
|
+
}
|
|
396
|
+
return String(value)
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
> **Note:** Color values use the 0-1 float range from Figma. Always multiply by 255 and round before hex conversion. See `design-to-code-visual.md` Section 1 for the RGB conversion formula.
|
|
401
|
+
|
|
402
|
+
#### Mode Value Extraction
|
|
403
|
+
|
|
404
|
+
For multi-mode collections, each variable's value is extracted per mode:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
async function extractModeValues(variableId, collections) {
|
|
408
|
+
const variable = await figma.variables.getVariableByIdAsync(variableId)
|
|
409
|
+
const collection = collections.find(c => c.id === variable.variableCollectionId)
|
|
410
|
+
|
|
411
|
+
// Single-mode collections: no per-mode extraction needed
|
|
412
|
+
if (!collection || collection.modes.length <= 1) {
|
|
413
|
+
return { collectionId: collection?.id }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Multi-mode: extract value for each mode
|
|
417
|
+
const modeValues = []
|
|
418
|
+
for (const mode of collection.modes) {
|
|
419
|
+
const modeValue = variable.valuesByMode[mode.id]
|
|
420
|
+
// Resolve aliases...
|
|
421
|
+
const stringValue = formatVariableValue(resolvedValue, variable.resolvedType)
|
|
422
|
+
modeValues.push({
|
|
423
|
+
modeId: mode.id,
|
|
424
|
+
modeName: mode.name,
|
|
425
|
+
value: stringValue,
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { collectionId: collection.id, modeValues }
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The result is an array of `{ modeId, modeName, value }` objects -- one per mode -- that the renderer uses to generate mode-aware CSS.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
### 6. Bound Variable Extraction
|
|
438
|
+
|
|
439
|
+
Bound variables are the connection between a Figma node's properties and the variable system. When a designer binds a variable to a property (e.g., binding `color/primary` to a frame's fill), the node's `boundVariables` field records this binding.
|
|
440
|
+
|
|
441
|
+
#### Extraction Sources
|
|
442
|
+
|
|
443
|
+
The extraction pipeline captures variable bindings from three property categories:
|
|
444
|
+
|
|
445
|
+
| Category | Properties | Intermediate Type Field |
|
|
446
|
+
|----------|-----------|------------------------|
|
|
447
|
+
| **Fill colors** | `paint.boundVariables.color` | `fill.variable` |
|
|
448
|
+
| **Layout spacing** | `node.boundVariables.itemSpacing`, `paddingTop`, etc. | `layout.gapVariable`, `layout.paddingVariables` |
|
|
449
|
+
| **Text fills** | `segment.fills[].variable` | `styledSegment.fills[].variable` |
|
|
450
|
+
|
|
451
|
+
#### Fill Variable Extraction
|
|
452
|
+
|
|
453
|
+
Fill variables are extracted at the paint level (each fill can have its own variable binding):
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
function collectVariableReferences(fill: FillData): void {
|
|
457
|
+
if (fill.type !== 'SOLID' || !fill.variable) return
|
|
458
|
+
const { id, name, isLocal } = fill.variable
|
|
459
|
+
// Deduplicate by variable ID
|
|
460
|
+
variableIdsToProcess.push({ id, name, value: fill.color, isLocal })
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The `fill.variable` field is populated during the extraction phase when `paint.boundVariables.color` is present on a solid fill.
|
|
465
|
+
|
|
466
|
+
#### Layout Variable Extraction
|
|
467
|
+
|
|
468
|
+
Layout variables (gap, padding) are extracted from the layout intermediate type:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// Gap variable
|
|
472
|
+
if (node.layout.gapVariable) {
|
|
473
|
+
addLayoutVariable(node.layout.gapVariable, node.layout.gap)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Counter axis gap variable
|
|
477
|
+
if (node.layout.counterAxisGapVariable && node.layout.counterAxisGap !== undefined) {
|
|
478
|
+
addLayoutVariable(node.layout.counterAxisGapVariable, node.layout.counterAxisGap)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Padding variables (per-side)
|
|
482
|
+
if (node.layout.paddingVariables) {
|
|
483
|
+
const pv = node.layout.paddingVariables
|
|
484
|
+
if (pv.top) addLayoutVariable(pv.top, node.layout.padding.top)
|
|
485
|
+
if (pv.right) addLayoutVariable(pv.right, node.layout.padding.right)
|
|
486
|
+
if (pv.bottom) addLayoutVariable(pv.bottom, node.layout.padding.bottom)
|
|
487
|
+
if (pv.left) addLayoutVariable(pv.left, node.layout.padding.left)
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
> For how layout variable bindings are extracted from Figma nodes, see `design-to-code-layout.md` Section 3.
|
|
492
|
+
|
|
493
|
+
#### Text Fill Variable Extraction
|
|
494
|
+
|
|
495
|
+
Styled text segments can have variable bindings on their fill colors:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
if (node.text?.styledSegments) {
|
|
499
|
+
for (const segment of node.text.styledSegments) {
|
|
500
|
+
if (segment.fills) {
|
|
501
|
+
for (const fill of segment.fills) {
|
|
502
|
+
if (fill.variable) {
|
|
503
|
+
variableIdsToProcess.push({
|
|
504
|
+
id: fill.variable.id,
|
|
505
|
+
name: fill.variable.name,
|
|
506
|
+
value: fill.color,
|
|
507
|
+
isLocal: fill.variable.isLocal ?? false,
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
> For text fill variable binding patterns, see `design-to-code-typography.md` Section 9.
|
|
517
|
+
|
|
518
|
+
#### Variable Binding Priority
|
|
519
|
+
|
|
520
|
+
When generating CSS for a property, the system checks for variable bindings before falling back to auto-detected tokens:
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
For a fill color:
|
|
524
|
+
1. fill.variable exists?
|
|
525
|
+
├─ YES, isLocal → var(--color-name) (no fallback)
|
|
526
|
+
├─ YES, external → var(--color-name, #rawColor) (with fallback)
|
|
527
|
+
└─ NO → check auto-detected token lookup
|
|
528
|
+
2. tokenLookup.colors has this value?
|
|
529
|
+
├─ YES → var(--color-primary) (from token system)
|
|
530
|
+
└─ NO → #rawColor (inline value)
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
This priority chain ensures explicit designer intent (variable bindings) always takes precedence over heuristic token detection.
|
|
534
|
+
|
|
535
|
+
> For the complete lookup chain implementation, see `design-tokens.md` Section 9.
|
|
536
|
+
|
|
537
|
+
#### Deduplication
|
|
538
|
+
|
|
539
|
+
Variables are deduplicated by ID before processing:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
const variableMap = new Map<string, FigmaVariableToken>()
|
|
543
|
+
// ... collect all references ...
|
|
544
|
+
for (const varRef of variableIdsToProcess) {
|
|
545
|
+
const cssName = figmaVariableToCssName(varRef.name)
|
|
546
|
+
const { collectionId, modeValues } = await extractModeValues(varRef.id, collections)
|
|
547
|
+
|
|
548
|
+
variableMap.set(varRef.id, {
|
|
549
|
+
name: cssName,
|
|
550
|
+
figmaName: varRef.name,
|
|
551
|
+
value: varRef.value,
|
|
552
|
+
isLocal: varRef.isLocal,
|
|
553
|
+
usageCount: 1,
|
|
554
|
+
collectionId,
|
|
555
|
+
modeValues,
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
If the same variable is bound to multiple nodes (e.g., `color/primary` used on 10 different fills), it is collected once and produces one CSS custom property definition.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
### 7. CSS Custom Property Generation from Variables
|
|
565
|
+
|
|
566
|
+
Variable names from Figma use `/` as a path separator (e.g., `color/link/default`). These must be converted to valid CSS custom property names using `-` separators.
|
|
567
|
+
|
|
568
|
+
#### Name Conversion Algorithm
|
|
569
|
+
|
|
570
|
+
From `lookup.ts`:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
function figmaVariableToCssName(variableName: string): string {
|
|
574
|
+
return '--' + variableName
|
|
575
|
+
.toLowerCase() // Lowercase everything
|
|
576
|
+
.replace(/\//g, '-') // Slashes to dashes
|
|
577
|
+
.replace(/\s+/g, '-') // Spaces to dashes
|
|
578
|
+
.replace(/[^a-z0-9-_]/g, '') // Remove invalid characters
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### Conversion Examples
|
|
583
|
+
|
|
584
|
+
| Figma Variable Name | CSS Custom Property |
|
|
585
|
+
|---------------------|---------------------|
|
|
586
|
+
| `color/primary` | `--color-primary` |
|
|
587
|
+
| `color/link/default` | `--color-link-default` |
|
|
588
|
+
| `spacing/md` | `--spacing-md` |
|
|
589
|
+
| `Color/Background/Primary` | `--color-background-primary` |
|
|
590
|
+
| `radius/md` | `--radius-md` |
|
|
591
|
+
| `Typography/Heading/Size` | `--typography-heading-size` |
|
|
592
|
+
| `brand color/accent` | `--brand-color-accent` |
|
|
593
|
+
|
|
594
|
+
#### Collection Prefix Handling
|
|
595
|
+
|
|
596
|
+
The pipeline does **not** prepend the collection name to the variable's CSS name. The variable's own path is used directly:
|
|
597
|
+
|
|
598
|
+
```
|
|
599
|
+
Collection: "Spacing"
|
|
600
|
+
Variable: "spacing/md"
|
|
601
|
+
CSS name: --spacing-md (NOT --spacing-spacing-md)
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
This works because Figma designers typically include the category in the variable name path. If a collection named "Colors" contains a variable named "primary", the CSS name would be `--primary` (not `--colors-primary`). Designers should use paths like `color/primary` to get `--color-primary`.
|
|
605
|
+
|
|
606
|
+
#### `var()` Reference Generation
|
|
607
|
+
|
|
608
|
+
The CSS `var()` reference wraps the custom property name:
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
function figmaVariableToCss(variableName: string): string {
|
|
612
|
+
return `var(${figmaVariableToCssName(variableName)})`
|
|
613
|
+
}
|
|
614
|
+
// "color/link/default" → "var(--color-link-default)"
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
#### Consistency with Auto-Detected Token Names
|
|
618
|
+
|
|
619
|
+
The variable path conversion uses the same algorithm as the design-tokens module's naming conventions. This means:
|
|
620
|
+
|
|
621
|
+
- `color/primary` (variable) → `--color-primary` (same as auto-detected `--color-primary`)
|
|
622
|
+
- `spacing/4` (variable) → `--spacing-4` (same as auto-detected `--spacing-4`)
|
|
623
|
+
|
|
624
|
+
When both a variable token and an auto-detected token produce the same name, the variable token takes priority. The auto-detected token is effectively shadowed.
|
|
625
|
+
|
|
626
|
+
> For the complete token naming conventions, see `design-tokens.md` Section 8.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
### 8. Mode-Aware CSS Rendering
|
|
631
|
+
|
|
632
|
+
The CSS renderer generates different output structures based on the collection's mode type. All rendering follows a mobile-first, light-default approach.
|
|
633
|
+
|
|
634
|
+
#### Rendering Flow
|
|
635
|
+
|
|
636
|
+
From `render.ts`:
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
1. Open :root { ... } block
|
|
640
|
+
2. Render variables without collections (simple variables)
|
|
641
|
+
3. For each collection:
|
|
642
|
+
a. Render default mode values inside :root
|
|
643
|
+
4. Close :root }
|
|
644
|
+
5. For each breakpoint collection:
|
|
645
|
+
a. Render non-default modes as @media (min-width) blocks
|
|
646
|
+
6. For each theme collection:
|
|
647
|
+
a. Render non-default modes as prefers-color-scheme + [data-theme] blocks
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
#### Base `:root` Block
|
|
651
|
+
|
|
652
|
+
All variables render their default mode values in the base `:root` block:
|
|
653
|
+
|
|
654
|
+
```css
|
|
655
|
+
:root {
|
|
656
|
+
/* Theme */
|
|
657
|
+
--color-bg: #ffffff;
|
|
658
|
+
--color-text: #1a1a1a;
|
|
659
|
+
--color-primary: #1a73e8;
|
|
660
|
+
|
|
661
|
+
/* Spacing */
|
|
662
|
+
--spacing-sm: 8px;
|
|
663
|
+
--spacing-md: 16px;
|
|
664
|
+
--spacing-lg: 24px;
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Variables without mode values (single-mode collections) also render here.
|
|
669
|
+
|
|
670
|
+
#### Breakpoint Mode Rendering
|
|
671
|
+
|
|
672
|
+
For collections classified as `breakpoint`, non-default modes generate `@media (min-width)` blocks. The default mode (mobile = 0) is already in `:root`.
|
|
673
|
+
|
|
674
|
+
```css
|
|
675
|
+
/* Base: mobile values (from :root above) */
|
|
676
|
+
|
|
677
|
+
@media (min-width: 768px) {
|
|
678
|
+
:root {
|
|
679
|
+
/* Spacing — tablet overrides */
|
|
680
|
+
--spacing-sm: 12px;
|
|
681
|
+
--spacing-md: 24px;
|
|
682
|
+
--spacing-lg: 32px;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
@media (min-width: 1024px) {
|
|
687
|
+
:root {
|
|
688
|
+
/* Spacing — desktop overrides */
|
|
689
|
+
--spacing-sm: 16px;
|
|
690
|
+
--spacing-md: 32px;
|
|
691
|
+
--spacing-lg: 48px;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Key behaviors:**
|
|
697
|
+
- The default mode (smallest breakpoint, typically "mobile") is already in `:root`
|
|
698
|
+
- Modes with breakpoint value 0 (mobile) are skipped in the media query loop
|
|
699
|
+
- Breakpoint modes are sorted ascending for correct mobile-first cascade
|
|
700
|
+
- Each media query re-opens `:root` to override the base values
|
|
701
|
+
|
|
702
|
+
#### Theme Mode Rendering
|
|
703
|
+
|
|
704
|
+
For collections classified as `theme`, non-default modes generate **two** selectors: a `prefers-color-scheme` media query and a `[data-theme]` attribute selector.
|
|
705
|
+
|
|
706
|
+
```css
|
|
707
|
+
/* Base: light values (from :root above) */
|
|
708
|
+
|
|
709
|
+
@media (prefers-color-scheme: dark) {
|
|
710
|
+
:root {
|
|
711
|
+
--color-bg: #1a1a1a;
|
|
712
|
+
--color-text: #ffffff;
|
|
713
|
+
--color-primary: #5c9aff;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
[data-theme="dark"] {
|
|
718
|
+
--color-bg: #1a1a1a;
|
|
719
|
+
--color-text: #ffffff;
|
|
720
|
+
--color-primary: #5c9aff;
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Why dual selectors:**
|
|
725
|
+
|
|
726
|
+
1. **`prefers-color-scheme`** -- Automatic theme based on OS/browser preference. No JavaScript required.
|
|
727
|
+
2. **`[data-theme]`** -- Manual theme toggle via JavaScript (`document.documentElement.setAttribute('data-theme', 'dark')`). Overrides OS preference when set.
|
|
728
|
+
|
|
729
|
+
The `[data-theme]` selector has higher specificity than the media query, so manual selection takes precedence.
|
|
730
|
+
|
|
731
|
+
#### Dark Mode Detection
|
|
732
|
+
|
|
733
|
+
Only modes whose name contains "dark" get the `prefers-color-scheme: dark` media query. Other non-default theme modes (e.g., "Brand B") get only the `[data-theme]` attribute selector:
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
if (themeName.includes('dark')) {
|
|
737
|
+
// Generate @media (prefers-color-scheme: dark) { :root { ... } }
|
|
738
|
+
}
|
|
739
|
+
// Always generate [data-theme="..."] { ... }
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
#### Complete Rendered Output Example
|
|
743
|
+
|
|
744
|
+
A file with both a theme collection and a spacing collection:
|
|
745
|
+
|
|
746
|
+
```css
|
|
747
|
+
:root {
|
|
748
|
+
/* Theme */
|
|
749
|
+
--color-bg: #ffffff;
|
|
750
|
+
--color-text: #1a1a1a;
|
|
751
|
+
--color-primary: #1a73e8;
|
|
752
|
+
|
|
753
|
+
/* Spacing */
|
|
754
|
+
--spacing-sm: 8px;
|
|
755
|
+
--spacing-md: 16px;
|
|
756
|
+
--spacing-lg: 24px;
|
|
757
|
+
|
|
758
|
+
/* Colors (auto-detected) */
|
|
759
|
+
--color-neutral-100: #f5f5f5;
|
|
760
|
+
--color-neutral-800: #333333;
|
|
761
|
+
|
|
762
|
+
/* Font Families */
|
|
763
|
+
--font-primary: 'Inter', sans-serif;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@media (min-width: 768px) {
|
|
767
|
+
:root {
|
|
768
|
+
--spacing-sm: 12px;
|
|
769
|
+
--spacing-md: 24px;
|
|
770
|
+
--spacing-lg: 32px;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
@media (min-width: 1024px) {
|
|
775
|
+
:root {
|
|
776
|
+
--spacing-sm: 16px;
|
|
777
|
+
--spacing-md: 32px;
|
|
778
|
+
--spacing-lg: 48px;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
@media (prefers-color-scheme: dark) {
|
|
783
|
+
:root {
|
|
784
|
+
--color-bg: #1a1a1a;
|
|
785
|
+
--color-text: #ffffff;
|
|
786
|
+
--color-primary: #5c9aff;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
[data-theme="dark"] {
|
|
791
|
+
--color-bg: #1a1a1a;
|
|
792
|
+
--color-text: #ffffff;
|
|
793
|
+
--color-primary: #5c9aff;
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
Note how auto-detected tokens (colors, font families) and variable tokens coexist in the same `:root` block. Variable tokens are grouped by collection, auto-detected tokens are grouped by category.
|
|
798
|
+
|
|
799
|
+
> For the `:root` block structure and category ordering, see `design-tokens.md` Section 10.
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
### 9. Fallback Values
|
|
804
|
+
|
|
805
|
+
Fallback values in `var()` references protect against missing variable definitions. The fallback strategy depends on whether the variable is local or from an external library.
|
|
806
|
+
|
|
807
|
+
#### Local Variables: No Fallback
|
|
808
|
+
|
|
809
|
+
Local variables (defined in the same Figma file) are always included in the generated `tokens.css` output. Since the token definition and the `var()` reference ship together, fallbacks are unnecessary:
|
|
810
|
+
|
|
811
|
+
```css
|
|
812
|
+
/* Local variable — guaranteed to be defined in tokens.css */
|
|
813
|
+
background-color: var(--color-primary);
|
|
814
|
+
gap: var(--spacing-md);
|
|
815
|
+
border-radius: var(--radius-md);
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
#### External Library Variables: Always Include Fallback
|
|
819
|
+
|
|
820
|
+
External library variables (from a separate Figma file) may not have corresponding definitions in the current project's `tokens.css`. The library token definitions live in the library's file, not the consuming file. Include the resolved raw value as a fallback:
|
|
821
|
+
|
|
822
|
+
```css
|
|
823
|
+
/* External library variable — fallback protects against missing definition */
|
|
824
|
+
background-color: var(--color-link-default, #1a73e8);
|
|
825
|
+
color: var(--color-brand-primary, #6366F1);
|
|
826
|
+
gap: var(--spacing-external-md, 16px);
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
#### Fallback Extraction
|
|
830
|
+
|
|
831
|
+
The fallback value comes from the variable's resolved value at extraction time (the raw CSS value that the variable currently resolves to):
|
|
832
|
+
|
|
833
|
+
```typescript
|
|
834
|
+
function lookupFillColor(fill, lookup): ColorLookupResult {
|
|
835
|
+
if (fill.variable && fill.variable.name) {
|
|
836
|
+
const cssVarName = figmaVariableToCssName(fill.variable.name)
|
|
837
|
+
|
|
838
|
+
if (fill.variable.isLocal) {
|
|
839
|
+
return { value: `var(${cssVarName})` }
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// External: include raw value as fallback
|
|
843
|
+
return {
|
|
844
|
+
value: `var(${cssVarName}, ${fill.color})`,
|
|
845
|
+
comment: `/* fallback: "${fill.variable.name}" is from external library */`,
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { value: lookupColor(lookup, fill.color) }
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
#### Token File Inclusion
|
|
853
|
+
|
|
854
|
+
During promotion, only **local** Figma Variables are included in the token output file. External variables are excluded because their definitions belong to the library:
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
// From promote.ts
|
|
858
|
+
const figmaVariables = tokens.figmaVariables?.filter((v) => v.isLocal)
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
This means:
|
|
862
|
+
- Local variables appear in `tokens.css` as `:root { --name: value; }` definitions
|
|
863
|
+
- Local variables are referenced without fallbacks: `var(--name)`
|
|
864
|
+
- External variables do NOT appear in `tokens.css`
|
|
865
|
+
- External variables are referenced WITH fallbacks: `var(--name, rawValue)`
|
|
866
|
+
|
|
867
|
+
#### When to Use Fallbacks on Local Variables
|
|
868
|
+
|
|
869
|
+
While the default approach omits fallbacks for local variables, some projects may want fallbacks for resilience:
|
|
870
|
+
|
|
871
|
+
| Scenario | Recommendation |
|
|
872
|
+
|----------|---------------|
|
|
873
|
+
| Single project, tokens and components shipped together | No fallback needed |
|
|
874
|
+
| Component library consumed by multiple projects | Include fallback |
|
|
875
|
+
| Visual Builder / iframe isolation | Include fallback (token aliasing may break) |
|
|
876
|
+
| Development/preview mode | Include fallback (tokens may not be loaded) |
|
|
877
|
+
|
|
878
|
+
> For the Visual Builder token aliasing pattern that bridges this gap, see `css-strategy.md` Section 4.
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
### 10. Variable Scopes & Token Applicability
|
|
883
|
+
|
|
884
|
+
Figma variable scopes constrain which UI pickers display a variable in the Figma editor. While scopes do NOT prevent programmatic binding via API or plugin code, they signal the designer's intended usage for each variable.
|
|
885
|
+
|
|
886
|
+
#### Scope → CSS Property Mapping
|
|
887
|
+
|
|
888
|
+
| Variable Scope | Applies To | CSS Properties |
|
|
889
|
+
|---------------|-----------|----------------|
|
|
890
|
+
| `ALL_SCOPES` | Everything | Any property |
|
|
891
|
+
| `ALL_FILLS` | All fill types | `background-color`, `color`, `fill` |
|
|
892
|
+
| `FRAME_FILL` | Frame fills only | `background-color` |
|
|
893
|
+
| `SHAPE_FILL` | Shape fills only | `background-color`, `fill` |
|
|
894
|
+
| `TEXT_FILL` | Text fills only | `color` |
|
|
895
|
+
| `STROKE_COLOR` | Stroke colors | `border-color`, `outline-color` |
|
|
896
|
+
| `EFFECT_COLOR` | Effect colors | Color component of `box-shadow` |
|
|
897
|
+
| `CORNER_RADIUS` | Border radius | `border-radius` |
|
|
898
|
+
| `WIDTH_HEIGHT` | Dimensions | `width`, `height` |
|
|
899
|
+
| `GAP` | Spacing/gap | `gap`, `row-gap`, `column-gap`, `padding`, `margin` |
|
|
900
|
+
| `OPACITY` | Opacity | `opacity` |
|
|
901
|
+
| `STROKE_FLOAT` | Stroke width | `border-width`, `outline-width` |
|
|
902
|
+
| `EFFECT_FLOAT` | Effect values | `blur()` radius, shadow spread |
|
|
903
|
+
| `FONT_FAMILY` | Font family | `font-family` |
|
|
904
|
+
| `FONT_STYLE` | Font style | `font-style` |
|
|
905
|
+
| `FONT_WEIGHT` | Font weight | `font-weight` |
|
|
906
|
+
| `FONT_SIZE` | Font size | `font-size` |
|
|
907
|
+
| `LINE_HEIGHT` | Line height | `line-height` |
|
|
908
|
+
| `LETTER_SPACING` | Letter spacing | `letter-spacing` |
|
|
909
|
+
|
|
910
|
+
#### Scope-Based Token Filtering
|
|
911
|
+
|
|
912
|
+
Variable scopes can be used to filter which tokens apply to which CSS properties. A variable scoped to `TEXT_FILL` should not be used as a `background-color` token, even if the color value matches.
|
|
913
|
+
|
|
914
|
+
However, the current recommended approach does not filter by scope -- it uses all variables as general-purpose tokens. Scope-based filtering is a future enhancement that would improve token specificity.
|
|
915
|
+
|
|
916
|
+
#### Scope Validity by Type
|
|
917
|
+
|
|
918
|
+
Only certain scopes are valid for each `resolvedType`:
|
|
919
|
+
|
|
920
|
+
| resolvedType | Valid Scopes |
|
|
921
|
+
|-------------|-------------|
|
|
922
|
+
| `COLOR` | `ALL_SCOPES`, `ALL_FILLS`, `FRAME_FILL`, `SHAPE_FILL`, `TEXT_FILL`, `STROKE_COLOR`, `EFFECT_COLOR` |
|
|
923
|
+
| `FLOAT` | `ALL_SCOPES`, `CORNER_RADIUS`, `WIDTH_HEIGHT`, `GAP`, `OPACITY`, `STROKE_FLOAT`, `EFFECT_FLOAT`, `FONT_WEIGHT`, `FONT_SIZE`, `LINE_HEIGHT`, `LETTER_SPACING` |
|
|
924
|
+
| `STRING` | `ALL_SCOPES`, `TEXT_CONTENT`, `FONT_FAMILY`, `FONT_STYLE` |
|
|
925
|
+
| `BOOLEAN` | `ALL_SCOPES` |
|
|
926
|
+
|
|
927
|
+
> For the complete scope reference and API details, see `figma-api-variables.md` Section "VariableScope".
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
### 11. Multi-Mode Design Patterns
|
|
932
|
+
|
|
933
|
+
Multi-mode variables enable designs that adapt to different contexts without changing component structure. The token system supports several patterns.
|
|
934
|
+
|
|
935
|
+
#### Pattern 1: Responsive Spacing
|
|
936
|
+
|
|
937
|
+
The same spacing variable has different values per breakpoint. Components automatically adapt when the viewport changes because they reference `var(--spacing-md)`:
|
|
938
|
+
|
|
939
|
+
```
|
|
940
|
+
Collection: "Spacing"
|
|
941
|
+
Modes: [Mobile, Tablet, Desktop]
|
|
942
|
+
|
|
943
|
+
Variable: spacing/md
|
|
944
|
+
Mobile: 16px
|
|
945
|
+
Tablet: 24px
|
|
946
|
+
Desktop: 32px
|
|
947
|
+
|
|
948
|
+
Variable: spacing/lg
|
|
949
|
+
Mobile: 24px
|
|
950
|
+
Tablet: 32px
|
|
951
|
+
Desktop: 48px
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**CSS Output:**
|
|
955
|
+
|
|
956
|
+
```css
|
|
957
|
+
:root {
|
|
958
|
+
--spacing-md: 16px;
|
|
959
|
+
--spacing-lg: 24px;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
@media (min-width: 768px) {
|
|
963
|
+
:root {
|
|
964
|
+
--spacing-md: 24px;
|
|
965
|
+
--spacing-lg: 32px;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
@media (min-width: 1024px) {
|
|
970
|
+
:root {
|
|
971
|
+
--spacing-md: 32px;
|
|
972
|
+
--spacing-lg: 48px;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**Component CSS (unchanged across breakpoints):**
|
|
978
|
+
|
|
979
|
+
```css
|
|
980
|
+
.card {
|
|
981
|
+
padding: var(--spacing-md);
|
|
982
|
+
gap: var(--spacing-lg);
|
|
983
|
+
}
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
#### Pattern 2: Light/Dark Theme Colors
|
|
987
|
+
|
|
988
|
+
Color variables switch between light and dark palettes. The component CSS stays identical:
|
|
989
|
+
|
|
990
|
+
```
|
|
991
|
+
Collection: "Theme"
|
|
992
|
+
Modes: [Light, Dark]
|
|
993
|
+
|
|
994
|
+
Variable: color/bg
|
|
995
|
+
Light: #ffffff
|
|
996
|
+
Dark: #1a1a1a
|
|
997
|
+
|
|
998
|
+
Variable: color/text
|
|
999
|
+
Light: #1a1a1a
|
|
1000
|
+
Dark: #ffffff
|
|
1001
|
+
|
|
1002
|
+
Variable: color/primary
|
|
1003
|
+
Light: #1a73e8
|
|
1004
|
+
Dark: #5c9aff
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
**CSS Output:**
|
|
1008
|
+
|
|
1009
|
+
```css
|
|
1010
|
+
:root {
|
|
1011
|
+
--color-bg: #ffffff;
|
|
1012
|
+
--color-text: #1a1a1a;
|
|
1013
|
+
--color-primary: #1a73e8;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
@media (prefers-color-scheme: dark) {
|
|
1017
|
+
:root {
|
|
1018
|
+
--color-bg: #1a1a1a;
|
|
1019
|
+
--color-text: #ffffff;
|
|
1020
|
+
--color-primary: #5c9aff;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
[data-theme="dark"] {
|
|
1025
|
+
--color-bg: #1a1a1a;
|
|
1026
|
+
--color-text: #ffffff;
|
|
1027
|
+
--color-primary: #5c9aff;
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
#### Pattern 3: Responsive Typography
|
|
1032
|
+
|
|
1033
|
+
Font sizes and line heights that scale with viewport:
|
|
1034
|
+
|
|
1035
|
+
```
|
|
1036
|
+
Collection: "Typography"
|
|
1037
|
+
Modes: [Mobile, Desktop]
|
|
1038
|
+
|
|
1039
|
+
Variable: text/body
|
|
1040
|
+
Mobile: 16px
|
|
1041
|
+
Desktop: 18px
|
|
1042
|
+
|
|
1043
|
+
Variable: text/heading
|
|
1044
|
+
Mobile: 28px
|
|
1045
|
+
Desktop: 40px
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
**CSS Output:**
|
|
1049
|
+
|
|
1050
|
+
```css
|
|
1051
|
+
:root {
|
|
1052
|
+
--text-body: 16px;
|
|
1053
|
+
--text-heading: 28px;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
@media (min-width: 1024px) {
|
|
1057
|
+
:root {
|
|
1058
|
+
--text-body: 18px;
|
|
1059
|
+
--text-heading: 40px;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
#### Pattern 4: Brand Theming
|
|
1065
|
+
|
|
1066
|
+
Multiple brand variants sharing a common token structure:
|
|
1067
|
+
|
|
1068
|
+
```
|
|
1069
|
+
Collection: "Brand"
|
|
1070
|
+
Modes: [Brand A, Brand B, Brand C]
|
|
1071
|
+
|
|
1072
|
+
Variable: color/primary
|
|
1073
|
+
Brand A: #0066ff
|
|
1074
|
+
Brand B: #ff3366
|
|
1075
|
+
Brand C: #00cc88
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
Brand modes do not match theme or breakpoint patterns, so they are classified as `unknown`. The pipeline renders only the default mode (Brand A) in `:root`. For multi-brand support, custom rendering logic is needed to generate class-based selectors:
|
|
1079
|
+
|
|
1080
|
+
```css
|
|
1081
|
+
/* Default (Brand A) */
|
|
1082
|
+
:root {
|
|
1083
|
+
--color-primary: #0066ff;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/* Custom rendering (not auto-generated) */
|
|
1087
|
+
.brand-b { --color-primary: #ff3366; }
|
|
1088
|
+
.brand-c { --color-primary: #00cc88; }
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
> For the class-based brand theming pattern, see `figma-api-variables.md` Section "Pattern: Brand Theming".
|
|
1092
|
+
|
|
1093
|
+
#### Pattern 5: Combined Responsive + Theme (Cross-Product)
|
|
1094
|
+
|
|
1095
|
+
When a file has both breakpoint and theme collections, they produce independent CSS blocks that compose naturally:
|
|
1096
|
+
|
|
1097
|
+
```
|
|
1098
|
+
Collection: "Theme" (theme mode type)
|
|
1099
|
+
Modes: [Light, Dark]
|
|
1100
|
+
Variables: color tokens...
|
|
1101
|
+
|
|
1102
|
+
Collection: "Spacing" (breakpoint mode type)
|
|
1103
|
+
Modes: [Mobile, Tablet, Desktop]
|
|
1104
|
+
Variables: spacing tokens...
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**CSS Output:**
|
|
1108
|
+
|
|
1109
|
+
```css
|
|
1110
|
+
:root {
|
|
1111
|
+
/* Theme — Light (default) */
|
|
1112
|
+
--color-bg: #ffffff;
|
|
1113
|
+
--color-text: #1a1a1a;
|
|
1114
|
+
|
|
1115
|
+
/* Spacing — Mobile (default) */
|
|
1116
|
+
--spacing-md: 16px;
|
|
1117
|
+
--spacing-lg: 24px;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/* Spacing overrides (responsive) */
|
|
1121
|
+
@media (min-width: 768px) {
|
|
1122
|
+
:root {
|
|
1123
|
+
--spacing-md: 24px;
|
|
1124
|
+
--spacing-lg: 32px;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
@media (min-width: 1024px) {
|
|
1129
|
+
:root {
|
|
1130
|
+
--spacing-md: 32px;
|
|
1131
|
+
--spacing-lg: 48px;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/* Theme overrides (dark) */
|
|
1136
|
+
@media (prefers-color-scheme: dark) {
|
|
1137
|
+
:root {
|
|
1138
|
+
--color-bg: #1a1a1a;
|
|
1139
|
+
--color-text: #ffffff;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
[data-theme="dark"] {
|
|
1144
|
+
--color-bg: #1a1a1a;
|
|
1145
|
+
--color-text: #ffffff;
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
These compose correctly because:
|
|
1150
|
+
- Spacing media queries override spacing tokens (independent of theme)
|
|
1151
|
+
- Theme selectors override color tokens (independent of viewport)
|
|
1152
|
+
- A dark-theme user on a tablet gets both dark colors AND tablet spacing
|
|
1153
|
+
|
|
1154
|
+
#### Limitations of Cross-Product
|
|
1155
|
+
|
|
1156
|
+
Figma does not support cross-product modes within a single collection (e.g., you cannot have "Light Mobile" and "Dark Desktop" as modes in one collection). The cross-product emerges naturally from having separate collections with independent mode axes.
|
|
1157
|
+
|
|
1158
|
+
If a design needs values that vary by **both** theme and breakpoint simultaneously (e.g., different spacing in dark mode), this requires either:
|
|
1159
|
+
- Two separate collections that compose
|
|
1160
|
+
- Custom post-processing to merge the outputs
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
### 12. Integration with Token Pipeline
|
|
1165
|
+
|
|
1166
|
+
Variables integrate with the existing token pipeline at two points: collection (Stage 1) and promotion (Stage 2).
|
|
1167
|
+
|
|
1168
|
+
#### Collection Integration
|
|
1169
|
+
|
|
1170
|
+
Variables are collected alongside auto-detected tokens in `collectAllTokens`:
|
|
1171
|
+
|
|
1172
|
+
```typescript
|
|
1173
|
+
async function collectAllTokens(node: ExtractedNode): Promise<DesignTokens> {
|
|
1174
|
+
const variableCollections = await getVariableCollections()
|
|
1175
|
+
|
|
1176
|
+
return {
|
|
1177
|
+
colors: collectColors(node), // Auto-detected
|
|
1178
|
+
spacing: collectSpacing(node), // Auto-detected
|
|
1179
|
+
typography: collectTypography(node), // Auto-detected
|
|
1180
|
+
effects: collectEffects(node), // Auto-detected
|
|
1181
|
+
figmaVariables: await collectFigmaVariables(node, variableCollections), // Explicit
|
|
1182
|
+
variableCollections, // Mode metadata
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
Variable collections are fetched first because the `collectFigmaVariables` function needs mode information to extract per-mode values.
|
|
1188
|
+
|
|
1189
|
+
#### Promotion Integration
|
|
1190
|
+
|
|
1191
|
+
During promotion, variables and auto-detected tokens are filtered differently:
|
|
1192
|
+
|
|
1193
|
+
```typescript
|
|
1194
|
+
function promoteTokens(tokens: DesignTokens, threshold: number = 2): DesignTokens {
|
|
1195
|
+
// Auto-detected tokens: filter by usage count
|
|
1196
|
+
const colors = tokens.colors.filter(c => c.usageCount >= threshold)
|
|
1197
|
+
const spacing = tokens.spacing.filter(s => s.usageCount >= threshold)
|
|
1198
|
+
// ... etc.
|
|
1199
|
+
|
|
1200
|
+
// Figma variables: ALWAYS included (explicit bindings), but only LOCAL
|
|
1201
|
+
const figmaVariables = tokens.figmaVariables?.filter(v => v.isLocal)
|
|
1202
|
+
|
|
1203
|
+
return { colors, spacing, typography, effects, figmaVariables, variableCollections }
|
|
1204
|
+
}
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
Key differences:
|
|
1208
|
+
|
|
1209
|
+
| Aspect | Auto-Detected Tokens | Figma Variable Tokens |
|
|
1210
|
+
|--------|:---:|:---:|
|
|
1211
|
+
| Promotion filter | Usage count >= threshold | Always promoted |
|
|
1212
|
+
| Local/external | N/A (always from current file) | Only local included |
|
|
1213
|
+
| Naming | Heuristic (HSL, scale, role) | From variable path |
|
|
1214
|
+
| Mode values | None (single value) | Per-mode values |
|
|
1215
|
+
| Conflict priority | Lower | Higher (wins conflicts) |
|
|
1216
|
+
|
|
1217
|
+
#### Rendering Integration
|
|
1218
|
+
|
|
1219
|
+
The `renderTokensCSS` function renders both token types in a single output:
|
|
1220
|
+
|
|
1221
|
+
1. **`:root` block** contains:
|
|
1222
|
+
- Figma Variables (default mode values), grouped by collection
|
|
1223
|
+
- Auto-detected tokens (colors, spacing, typography, effects), grouped by category
|
|
1224
|
+
|
|
1225
|
+
2. **Media query blocks** contain:
|
|
1226
|
+
- Breakpoint mode overrides (Figma Variables only)
|
|
1227
|
+
|
|
1228
|
+
3. **Theme selector blocks** contain:
|
|
1229
|
+
- Theme mode overrides (Figma Variables only)
|
|
1230
|
+
|
|
1231
|
+
Auto-detected tokens do not have mode-aware rendering -- they always appear in the base `:root` block with a single value.
|
|
1232
|
+
|
|
1233
|
+
#### Lookup Integration
|
|
1234
|
+
|
|
1235
|
+
The token lookup system checks variable bindings first, then falls back to auto-detected tokens:
|
|
1236
|
+
|
|
1237
|
+
```
|
|
1238
|
+
lookupFillColor(fill, lookup):
|
|
1239
|
+
1. fill.variable exists? → var(--css-name) [± fallback]
|
|
1240
|
+
2. lookup.colors has this value? → var(--color-token)
|
|
1241
|
+
3. Neither → raw color value
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
This means a color that is both:
|
|
1245
|
+
- Bound to a variable named `color/link/default`
|
|
1246
|
+
- Auto-detected as `--color-primary` (most-used saturated color)
|
|
1247
|
+
|
|
1248
|
+
...will render as `var(--color-link-default)`, not `var(--color-primary)`. The explicit variable binding wins.
|
|
1249
|
+
|
|
1250
|
+
> For the complete token pipeline stages, see `design-tokens.md` Section 1.
|
|
1251
|
+
|
|
1252
|
+
---
|
|
1253
|
+
|
|
1254
|
+
## Cross-References
|
|
1255
|
+
|
|
1256
|
+
- **`figma-api-variables.md`** -- Variables API endpoints, data model (Variable, VariableCollection, modes, scopes), alias resolution mechanics, multi-mode theming patterns, access requirements (Enterprise plan). The API-level reference for all Variables operations.
|
|
1257
|
+
- **`design-tokens.md`** -- General token extraction pipeline (collect, promote, name, render), threshold-based promotion, HSL-based color naming, spacing scale detection, token lookup system, CSS/SCSS/Tailwind rendering. The pipeline that Variables plug into.
|
|
1258
|
+
- **`css-strategy.md`** -- Three-layer CSS architecture (Tailwind + Custom Properties + CSS Modules), property placement decision tree, specificity management, responsive strategy, theme support, Visual Builder token aliasing. Where generated tokens are consumed.
|
|
1259
|
+
- **`design-to-code-visual.md`** -- Visual property extraction (fills, strokes, effects, corners) with variable binding resolution. Fill variable priority chain. HSL-based color token naming.
|
|
1260
|
+
- **`design-to-code-layout.md`** -- Layout property extraction (gap, padding) with variable bindings. Variable-aware spacing in CSS generation. Mobile-first responsive pattern.
|
|
1261
|
+
- **`design-to-code-typography.md`** -- Typography extraction with variable bindings on text fill colors in styled segments. Font family/size/weight token lookup during generation.
|