A single semantic token - color/brand/primary - can hold three different hex values at once, one per brand, and resolve to exactly one of them depending on which Mode is active. That's what Figma Variables, shipped in 2023, replaced: parallel style libraries, one per brand, updated by hand every time a color changed. Pair Variables with the Figma REST API and every one of those tokens becomes something a build pipeline pulls down automatically, with no one copying hex codes out of an Inspect panel.
This guide covers the three-collection architecture that keeps a multi-brand system maintainable, the REST API calls that extract every variable programmatically, and two things that changed in how Figma handles variables over the past year.
1. Understanding Figma Variables
Figma Variables are typed tokens stored in a Variable Collection. Each collection has one or more Modes, and each variable holds one value per Mode.
Variable Types
The REST API's resolvedType field uses four values, and one of them doesn't match what the Figma UI shows you:
COLOR- hex/rgba values; used for fills, strokes, backgroundsFLOAT- spacing, border radius, font sizes, opacity. Figma's UI labels this type "Number" when you create a variable, but the API returnsresolvedType: "FLOAT", not"NUMBER"- a common source of silent bugs in token-transform scripts that switch on the raw type string.STRING- font families, icon names, locale stringsBOOLEAN- feature flags, visibility toggles
Variable Scopes
Each variable carries a scopes array that tells Figma where it can be applied: FILL_COLOR, STROKE_COLOR, CORNER_RADIUS, GAP, FONT_SIZE, etc. Scopes prevent a spacing token from accidentally being applied to a color field, keeping your system clean.
2. Designing a Multi-Brand System with Modes
A well-structured multi-brand system separates concerns into at least two Variable Collections:
Collection 1 - Primitives (no Modes needed)
Raw, alias-free values: every color in your palette, every step of your spacing scale, every font weight. These never change between brands.
primitive/blue-500→#0049B8primitive/space-4→4primitive/radius-sm→8
Collection 2 - Semantic / Brand Tokens (Modes = Brands)
Semantic tokens alias into Primitives and use Modes to resolve per-brand. Add one Mode per brand (Brand A, Brand B, Brand C). Each brand Mode maps the same semantic name to a different Primitive value.
brand/primary→ Brand A:primitive/blue-500| Brand B:primitive/purple-600brand/surface→ Brand A:#FFFFFF| Brand B:#0F0F1A
Collection 3 - Theme Tokens (Modes = Light / Dark)
A third collection handles appearance modes. Each variable aliases into the semantic brand token, which in turn aliases into a Primitive. This creates a clean three-level chain: Primitive → Brand → Theme.
color/background/page→ Light:brand/surface| Dark:primitive/gray-950color/text/primary→ Light:primitive/gray-900| Dark:primitive/gray-50
Setting the Active Mode in Figma
In the Design panel, select any frame and click the Variables icon. You'll see each collection with a Mode dropdown. Switch Brand Mode to "Brand B" and your entire frame instantly re-skins. Switch Theme Mode to "Dark" and all semantic color tokens resolve to their dark-mode aliases. No re-applying styles - just Mode switches.
When a Collection Outgrows Modes: Extended Collections
The Brand collection above works cleanly for two or three brands. Past ten or so - an agency running many client design systems, or a company with regional sub-brands - packing every brand into Modes on one collection turns it into an unmanageable wall of columns, and every brand inherits every other brand's naming decisions. Figma addressed this in November 2025 with extended collections: a collection can declare a parentVariableCollectionId, inheriting every variable and Mode from its parent while overriding only the values that differ for that instance. The REST API exposes the override set directly through a variableOverrides map on the extended collection, so you can see exactly what a given brand changed against the base system without walking the whole variable tree.
For two or three brands, the Primitive → Brand → Theme structure above is still the simpler choice. Reach for extended collections once brand count or override volume makes a single Modes-based collection unwieldy - and note the feature is Enterprise-only, same as the rest of the Variables system.
3. Figma REST API - Getting Variables
Once your system is solid in Figma, the REST API lets you pull every variable out programmatically. One restriction catches teams off guard: the Variables endpoints are available only to full members of an Enterprise org - a Professional or Organization-plan token gets a 403 even with a correctly scoped request. You'll also need the file_variables:read scope on your personal access token (the older file_read / files:read scopes are deprecated and won't grant variables access), and as of April 2025 Figma caps personal access tokens at a 90-day expiry - there's no more "never expires" option, so a CI pipeline built on a PAT needs a rotation step. The key endpoint:
GET https://api.figma.com/v1/files/:file_key/variables/local
X-Figma-Token: <FIGMA_PERSONAL_ACCESS_TOKEN>
This returns meta.variables (the tokens) and meta.variableCollections (the collections + Modes). Here is a minimal Node.js script to fetch and log them:
// fetch-figma-variables.mjs
import fetch from 'node-fetch';
const FILE_KEY = 'YOUR_FILE_KEY'; // from the Figma file URL
const API_TOKEN = process.env.FIGMA_TOKEN;
async function getFigmaVariables() {
const res = await fetch(
`https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
{ headers: { 'X-Figma-Token': API_TOKEN } }
);
if (!res.ok) throw new Error(`Figma API ${res.status}: ${await res.text()}`);
const { meta } = await res.json();
const { variables, variableCollections } = meta;
// Build a lookup: collectionId → { name, modes }
const collections = Object.fromEntries(
Object.values(variableCollections).map(c => [c.id, c])
);
// Print each variable with its values per Mode
for (const v of Object.values(variables)) {
const col = collections[v.variableCollectionId];
console.log(`[${col.name}] ${v.name} (${v.resolvedType})`);
for (const [modeId, value] of Object.entries(v.valuesByMode)) {
const modeName = col.modes.find(m => m.modeId === modeId)?.name ?? modeId;
console.log(` ${modeName}:`, value);
}
}
}
getFigmaVariables().catch(console.error);
4. Converting Figma Variables to Design Tokens (W3C Format)
The W3C Design Token Community Group defines a standard JSON format, and it uses number as the type name for numeric tokens - not float. Since the Figma API's resolvedType for numeric variables is literally the string "FLOAT", a naive variable.resolvedType.toLowerCase() conversion produces $type: "float", which isn't a valid W3C type and will silently break in tools like Style Dictionary that expect "number". Map the types explicitly instead - here's how to transform the Figma API response into valid W3C tokens, one file per Mode:
// transform-tokens.mjs
const TYPE_MAP = { COLOR: 'color', FLOAT: 'number', STRING: 'string', BOOLEAN: 'boolean' };
function toW3CValue(variable, rawValue) {
const w3cType = TYPE_MAP[variable.resolvedType] ?? variable.resolvedType.toLowerCase();
if (typeof rawValue === 'object' && rawValue.type === 'VARIABLE_ALIAS') {
// Reference to another token - use the aliased variable's name
return { $value: `{alias.${rawValue.id}}`, $type: w3cType };
}
if (variable.resolvedType === 'COLOR') {
const { r, g, b, a } = rawValue;
const toHex = n => Math.round(n * 255).toString(16).padStart(2, '0');
const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
return { $value: a < 1 ? `rgba(${Math.round(r*255)},${Math.round(g*255)},${Math.round(b*255)},${a})` : hex,
$type: 'color' };
}
return { $value: rawValue, $type: w3cType };
}
function buildTokenTree(variables, variableCollections, targetModeId) {
const tree = {};
for (const v of Object.values(variables)) {
const col = variableCollections[v.variableCollectionId];
const modeId = targetModeId ?? col.defaultModeId;
const rawValue = v.valuesByMode[modeId];
if (rawValue === undefined) continue;
// Nest by variable name path (e.g. "color/brand/primary" → tree.color.brand.primary)
const parts = v.name.split('/');
let node = tree;
for (const part of parts.slice(0, -1)) {
node[part] ??= {};
node = node[part];
}
node[parts.at(-1)] = toW3CValue(v, rawValue);
}
return tree;
}
// Usage:
// const lightTokens = buildTokenTree(variables, variableCollections, lightModeId);
// fs.writeFileSync('tokens/light.json', JSON.stringify(lightTokens, null, 2));
5. Plugging Tokens into Your Dev Workflow
Once you have the W3C-format JSON files, pipe them through Style Dictionary to auto-generate CSS custom properties, SCSS variables, or TypeScript constants - whatever your stack needs.
# tokens/
# light.json
# dark.json
# brand-a.json
# brand-b.json
# style-dictionary.config.mjs
export default {
source: ['tokens/light.json'],
platforms: {
css: {
transformGroup: 'css',
prefix: 'dt',
buildPath: 'src/styles/generated/',
files: [{ destination: 'light.css', format: 'css/variables' }]
},
ts: {
transformGroup: 'js',
buildPath: 'src/tokens/',
files: [{ destination: 'light.ts', format: 'javascript/es6' }]
}
}
};
6. Automating the Pipeline with GitHub Actions
Wire the whole thing into CI so tokens update automatically whenever the Figma file changes (via a Figma webhook) or on a nightly schedule:
# .github/workflows/sync-design-tokens.yml
name: Sync Design Tokens
on:
schedule:
- cron: '0 6 * * *' # every day at 6 AM UTC
workflow_dispatch: # manual trigger from GitHub UI
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: node scripts/fetch-figma-variables.mjs
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
FIGMA_FILE_KEY: ${{ secrets.FIGMA_FILE_KEY }}
- run: npx style-dictionary build
- uses: peter-evans/create-pull-request@v6
with:
title: 'chore: sync design tokens from Figma'
branch: 'design-tokens/auto-sync'
One operational note for CI use: Figma updated its REST API rate limits in November 2025, and file-read and variable-read endpoints sit in a higher-cost tier than lightweight metadata calls. A nightly cron job comfortably stays under the limit; if you trigger this workflow off a Figma webhook across a large team, add a debounce so you're not re-fetching the same file on every edit a designer makes.
Key Takeaways
- Three-level alias chain (Primitive → Brand → Theme) keeps your tokens composable and brand-swappable without duplication - and stays the simplest option for a handful of brands.
- Extended collections, added in November 2025, solve the same problem at higher brand counts, letting each brand override only what it changes instead of adding another Mode column to one giant collection.
- Variables access is Enterprise-only - confirm your org's plan and your token's
file_variables:read/file_variables:writescopes before building a pipeline around this API. - Match Figma's types to your token spec's types explicitly -
FLOATis not"number", and a blind.toLowerCase()produces invalid W3C tokens.
The result: designers own the token values in Figma, developers consume generated CSS/TS files, and both sides stay in sync automatically - no more copy-pasting hex codes from Zeplin or Inspect panels.


