Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Official Figma skill for writing directly to the Figma canvas through the MCP server and Plugin API.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
variable-patterns.md
1# Variable & Token API Patterns23> Part of the [use_figma skill](../SKILL.md). How to correctly create, bind, scope, and alias variables using the Plugin API.4>5> For design system context (aliasing strategy, mode decisions, code syntax philosophy, grouping conventions), see [wwds-variables](working-with-design-systems/wwds-variables.md).67## Contents89- Creating Variable Collections and Modes10- Creating Variables (All Types)11- Binding Variables to Node Properties12- Variable Scopes: What They Are and How to Set Them13- Variable Aliasing (VARIABLE_ALIAS)14- Code Syntax (setVariableCodeSyntax)15- Importing Library Variables16- Discovering Existing Variables in the File17- Effect Styles (For Shadows)181920## Creating Variable Collections and Modes2122```javascript23const collection = figma.variables.createVariableCollection("MyCollection");2425// A new collection starts with 1 mode named "Mode 1" — always rename it26collection.renameMode(collection.modes[0].modeId, "Light");2728// Add additional modes (returns the new modeId)29const darkModeId = collection.addMode("Dark");30const lightModeId = collection.modes[0].modeId;31```3233**Mode limits are plan-dependent:** Free = 1 mode, Professional = up to 4, Organization/Enterprise = 40+. If you need many modes, split across multiple collections.3435## Creating Variables (All Types)3637`figma.variables.createVariable(name, collection, resolvedType)` — the second argument accepts a collection object or ID string (object preferred).3839```javascript40// COLOR — values use {r, g, b, a} (all 0–1 range, includes alpha)41const colorVar = figma.variables.createVariable("my-color", collection, "COLOR");42colorVar.setValueForMode(modeId, { r: 0.2, g: 0.36, b: 0.96, a: 1 });4344// FLOAT — for spacing, radii, sizing, numeric values45const floatVar = figma.variables.createVariable("my-spacing", collection, "FLOAT");46floatVar.setValueForMode(modeId, 16);4748// STRING — for font families, font style names, any text value49const stringVar = figma.variables.createVariable("my-font", collection, "STRING");50stringVar.setValueForMode(modeId, "Inter");5152// BOOLEAN53const boolVar = figma.variables.createVariable("my-flag", collection, "BOOLEAN");54boolVar.setValueForMode(modeId, true);55```5657**Note:** Paint colors use `{r, g, b}` (no alpha), but COLOR variable values use `{r, g, b, a}` (with alpha). Don't mix them up.5859## Binding Variables to Node Properties6061### Color Bindings (Fills, Strokes)6263`setBoundVariableForPaint` returns a **NEW paint** — you must capture the return value:6465```javascript66// Create a base paint, bind the variable, assign the result67const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } };68const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar);69node.fills = [boundPaint];7071// Only SOLID paints support color variable binding — gradients/images will throw72```7374### Numeric Bindings (Spacing, Radii, Sizing)7576`setBoundVariable` binds FLOAT/STRING/BOOLEAN variables to node properties:7778```javascript79// Padding80node.setBoundVariable("paddingTop", spacingVar);81node.setBoundVariable("paddingBottom", spacingVar);82node.setBoundVariable("paddingLeft", spacingVar);83node.setBoundVariable("paddingRight", spacingVar);8485// Gap86node.setBoundVariable("itemSpacing", gapVar);87node.setBoundVariable("counterAxisSpacing", gapVar);8889// Corner radius — use individual corners, NOT cornerRadius90node.setBoundVariable("topLeftRadius", radiusVar);91node.setBoundVariable("topRightRadius", radiusVar);92node.setBoundVariable("bottomLeftRadius", radiusVar);93node.setBoundVariable("bottomRightRadius", radiusVar);9495// Size96node.setBoundVariable("width", sizeVar);97node.setBoundVariable("height", sizeVar);98node.setBoundVariable("minWidth", sizeVar);99node.setBoundVariable("maxWidth", sizeVar);100101// Other102node.setBoundVariable("opacity", opacityVar);103node.setBoundVariable("strokeWeight", strokeVar);104```105106**Not bindable via setBoundVariable:** `fontSize`, `fontWeight`, `lineHeight` — set these directly on text nodes.107108### Effect Bindings109110```javascript111const effectCopy = JSON.parse(JSON.stringify(node.effects[0]));112const newEffect = figma.variables.setBoundVariableForEffect(effectCopy, "color", colorVar);113// ⚠️ Returns a NEW effect — must capture return value!114node.effects = [newEffect];115// Valid fields: "color" (COLOR), "radius" | "spread" | "offsetX" | "offsetY" (FLOAT)116```117118### Applying a Mode to a Frame119120```javascript121// All bound children of this frame will resolve to the specified mode's values122frame.setExplicitVariableModeForCollection(collection, modeId);123```124125Without this, all nodes use the collection's default (first) mode.126127## Variable Scopes: What They Are and How to Set Them128129`variable.scopes` controls which Figma property pickers show the variable. The default is `["ALL_SCOPES"]` which shows it everywhere — this is almost never what you want.130131```javascript132variable.scopes = ["FRAME_FILL", "SHAPE_FILL"]; // only fill pickers133variable.scopes = ["TEXT_FILL"]; // only text color picker134variable.scopes = ["GAP"]; // only gap/spacing pickers135variable.scopes = ["CORNER_RADIUS"]; // only radius pickers136variable.scopes = []; // hidden from all pickers137```138139**All valid scope values:**140`ALL_SCOPES`, `TEXT_CONTENT`, `CORNER_RADIUS`, `WIDTH_HEIGHT`, `GAP`, `ALL_FILLS`, `FRAME_FILL`, `SHAPE_FILL`, `TEXT_FILL`, `STROKE_COLOR`, `STROKE_FLOAT`, `EFFECT_FLOAT`, `EFFECT_COLOR`, `OPACITY`, `FONT_FAMILY`, `FONT_STYLE`, `FONT_WEIGHT`, `FONT_SIZE`, `LINE_HEIGHT`, `LETTER_SPACING`, `PARAGRAPH_SPACING`, `PARAGRAPH_INDENT`141142**Always set scopes explicitly** — `ALL_SCOPES` is the default but almost never what you want. For a comprehensive scope-to-use-case mapping table, see [token-creation.md § Variable Scopes — Complete Reference Table](../../figma-generate-library/references/token-creation.md).143144**Always check the existing file's scope patterns before creating variables** — match whatever convention is already in use. See "Discovering Existing Variables" below.145146## Variable Aliasing (VARIABLE_ALIAS)147148A variable's value can reference another variable via alias. This is how semantic tokens reference primitive tokens:149150```javascript151// Set a variable's value as an alias to another variable152semanticVar.setValueForMode(modeId, {153type: 'VARIABLE_ALIAS',154id: primitiveVar.id155});156```157158When the primitive changes, the semantic variable updates automatically across all modes.159160## Code Syntax (setVariableCodeSyntax)161162Links a Figma variable back to its code counterpart. Call once per platform:163164```javascript165variable.setVariableCodeSyntax('WEB', 'var(--color-bg-default)');166variable.setVariableCodeSyntax('ANDROID', 'colorBgDefault');167variable.setVariableCodeSyntax('iOS', 'Color.bgDefault');168169// Read back: variable.codeSyntax → { WEB: '...', ANDROID: '...', iOS: '...' }170```171172**When deriving CSS names from Figma names**, replace both slashes AND spaces with hyphens:173174```javascript175// WRONG — leaves spaces in CSS variable name176`var(--${figmaName.replace(/\//g, '-').toLowerCase()})`177178// CORRECT — replace all whitespace and slashes179`var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()})`180181// BEST — use the original CSS variable name from the source, not a derived one182`var(${token.cssVar})`183```184185## Importing Library Variables186187For variables from **team libraries** (not the current file), use `importVariableByKeyAsync`:188189```javascript190// Import a single variable by its key191const colorVar = await figma.variables.importVariableByKeyAsync("VARIABLE_KEY");192// Now use it like any local variable193const paint = figma.variables.setBoundVariableForPaint(194{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', colorVar195);196node.fills = [paint];197```198199To discover available library variable collections and their variables:200201```javascript202// List all available library variable collections203const libCollections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();204// Each has: name, key, libraryName205206// Get variables in a specific library collection207const libVars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(libCollections[0].key);208// Each has: name, key, resolvedType209// Import the ones you need:210const imported = await figma.variables.importVariableByKeyAsync(libVars[0].key);211```212213**When to import vs. use local:** If `variable.remote === true`, it's from a library — you can reference it directly if already imported, or import by key. If `remote === false`, it's local to the file — use `getVariableByIdAsync` directly.214215## Discovering Existing Variables in the File216217**Always inspect the file's existing variables before creating new ones.** Different files use different naming conventions, scope patterns, and collection structures. Match what's already there.218219### List collections with mode info220221```javascript222const collections = await figma.variables.getLocalVariableCollectionsAsync();223const results = collections.map(c => ({224name: c.name,225id: c.id,226varCount: c.variableIds.length,227modes: c.modes.map(m => ({ name: m.name, id: m.modeId }))228}));229return results;230```231232### Inspect scope patterns used in existing variables233234```javascript235const collections = await figma.variables.getLocalVariableCollectionsAsync();236const scopeGroups = {};237for (const c of collections) {238for (const id of c.variableIds) {239const v = await figma.variables.getVariableByIdAsync(id);240const key = JSON.stringify(v.scopes);241if (!scopeGroups[key]) scopeGroups[key] = [];242scopeGroups[key].push(v.name);243}244}245return scopeGroups;246```247248### Build a name→variable lookup for reuse249250```javascript251const varByName = {};252for (const v of await figma.variables.getLocalVariablesAsync()) {253varByName[v.name] = v;254}255256// Bind to existing variable by name — no hex values needed257function bindFill(node, varName) {258const v = varByName[varName];259if (!v) throw new Error(`Variable not found: ${varName}`);260const paint = figma.variables.setBoundVariableForPaint(261{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', v262);263node.fills = [paint];264}265```266267**Only create new variables for tokens that have no match in the file.** After building the lookup, compare against the needed tokens and create variables only for the delta.268269## Listing Collections with Full Variable Details270271The async API returns richer data including code syntax and scopes per variable:272273```javascript274/**275* Lists all local variable collections defined in the current Figma file,276* including metadata for their modes and variables.277*278* @returns {Promise<Array<{279* name: string,280* id: string,281* modes: Array<[name: string, modeId: string]>,282* variables: Array<[name: string, id: string, codeSyntax: object, scopes: string[]]>283* }>>}284*/285async function listVariableCollectionsAndVariables() {286const collections = await figma.variables.getLocalVariableCollectionsAsync();287const results = [];288for (const collection of collections) {289const vars = [];290for (const id of collection.variableIds) {291const v = await figma.variables.getVariableByIdAsync(id);292vars.push([v.name, v.id, v.codeSyntax, v.scopes]);293}294results.push({295name: collection.name,296id: collection.id,297modes: collection.modes.map(m => [m.name, m.modeId]),298variables: vars299});300}301return results;302}303```304305Full runnable script:306307```javascript308const results = await listVariableCollectionsAndVariables();309return results;310```311312## Setting and Removing Code Syntax313314Must be executed in the file the variable is defined in:315316```javascript317/**318* Set the code syntax for a variable for a specific platform.319*320* @param {string} variableId321* @param {'WEB'|'ANDROID'|'iOS'} platform322* @param {string} syntax323*/324async function setVariableCodeSyntax(variableId, platform, syntax) {325const variable = await figma.variables.getVariableByIdAsync(variableId);326variable.setVariableCodeSyntax(platform, syntax);327}328329/**330* Remove code syntax for a variable for one or more platforms.331*332* @param {string} variableId333* @param {Array<'WEB'|'ANDROID'|'iOS'>} platforms — defaults to all three334*/335async function removeVariableCodeSyntax(variableId, platforms = ["WEB", "ANDROID", "iOS"]) {336const variable = await figma.variables.getVariableByIdAsync(variableId);337for (const platform of platforms) {338variable.removeVariableCodeSyntax(platform);339}340}341342/**343* Set a value for a variable in a specific mode.344* For aliases, value must be: { type: 'VARIABLE_ALIAS', id: '<variableId>' }345*346* @param {string} variableId347* @param {string} modeId348* @param {string|number|boolean|RGB|RGBA|{type: 'VARIABLE_ALIAS', id: string}} value349*/350async function setVariableValueForMode(variableId, modeId, value) {351const variable = await figma.variables.getVariableByIdAsync(variableId);352variable.setValueForMode(modeId, value);353}354```355356## Effect Styles (For Shadows)357358Shadows can't be stored as variables. Use effect styles. For comprehensive patterns, see [effect-style-patterns.md](effect-style-patterns.md).359360```javascript361const shadow = figma.createEffectStyle();362shadow.name = "Shadow/Subtle";363shadow.effects = [{364type: "DROP_SHADOW",365color: { r: 0, g: 0, b: 0, a: 0.06 },366offset: { x: 0, y: 2 },367radius: 8,368spread: 0,369visible: true,370blendMode: "NORMAL"371}];372373// Apply to a node374frame.effectStyleId = shadow.id;375```376