Design tokens logo, magic want icon, and Figma logo in line to signify the magic of a pipeline between tokens to Figma variables.

Synchronizing Figma variables with Design Tokens

Nate Baldwin
12 min readOct 17, 2023

--

Summer of 2023, Figma announced Figma variables: an internal feature that mirrors the functionality of Tokens within Figma, along with REST API support for developers. Figma variables is currently still in Beta and only available to educational and paid plans, but even if you’re not on those plans, I hope this article provides some insight into what’s available with these new features.

In this article, I am going to share context and the problem we face with Intuit’s design system, initial Figma setup, a several methods used to integrate with the Figma API, a specific wrench to watch out for in the process, and some of the challenges we still face moving forward.

Context and the problem

Intuit’s design system acts as a core system for a multi-brand and multi-platform ecosystem. We support product-specific design systems and brands like TurboTax, Quickbooks, Mailchimp, and CreditKarma. All of the styles for these brands are maintained in a single source of Design tokens.

Columns of themed buttons, presented with different colors and labeled by their theme (Intuit, Quickbooks, TurboTax, Mailchimp, and CreditKarma)

Design tokens solve many problems for our products, but we lack the ability to share this functionality with designers. We also lack a direct pipeline for design tokens to publish into design resources. As a result, designers cannot test different themes or color modes, and we have to manually keep design libraries and resources up-to-date with our design tokens.

Figma’s REST API offers us a promising opportunity to finally tie our design tokens directly to design resources and integrate Figma as part of a CI/CD pipeline. And variables themselves offer the opportunity for designers to experience and gain a better understanding of the power of design tokens.

Figma interface displaying a dropdown of theme options from a variables mode selector

Figma initial setup

To begin, we had to follow a few steps to get ready to test things out. We needed a Figma access key, a new file and the file’s file key:

1. Create a Figma access key

For this, I used my own personal token for the time being. Go to your profile settings in Figma. Scroll down the modal and you will find a section called “Access keys”. Click “create new key”, and set the expiration to never.

Modal dialog within Figma settings with section Personal access tokens in view and a box highlighting the button “Generate new token” and the token labeled Vairables webhooks.

2. Create a Figma file for your library

Just as we’ve done with styles libraries, we needed a Figma file that would house our new variables. After creating this new file (which we’ve titled “Tokens”), we need the File key. Go to your file’s “Share” button and copy the link to the file. Within the URL that is copied to your clipboard is a unique string. It’s located between the Figma URL and the file’s name — this is the file key.

Figma share dialog with button labeled “copy link” highlighted

Your share file link will look something like this, with the file key in bold:

https://www.figma.com/file/XXXxxxXxxXxx0xX0XX0XXX/Tokens?type=design&node-id=0%3A1&mode=design&t=jRXFjAaKX09yFC4I-1

Integrating with the REST API

In order to integrate the Figma REST API into our Design Token system, we had to do a couple of things:

  1. Create basic API call structure
  2. Read our tokens
  3. Understanding the POST format
  4. Create the “Themes” collection
  5. Capture and store variable IDs

There’s a lot more that we had to do in order to resolve complex use cases and inconsistencies in the output of our Design tokens, as well as identifying ways to synchronize our script with a file that already has Figma variables named as our tokens, but I’m not going to cover all that here.

Create basic API call structure

Here’s where I had to go on a little learning-journey of my own. As a designer, REST APIs are completely new to me. I leveraged Axios to help with the calls in Javascript.

Our basic function to make POST (or to publish to the Figma API) will look like this:

const fileKey = 'XXXxxxXxxXxx0xX0XX0XXX';
const figmaAccessToken = 'figd_X_XxxxXX0xX0xXXxXXXXxXXx0x00XxxX';
const postData = '';

axios
.post(`https://api.figma.com/v1/files/${fileKey}/variables`, postData, {
headers: {
'X-Figma-Token': figmaAccessToken,
},
})
.then(function (response) {
// what to do next
})
.catch(function (error) {
// log any errors
console.log(error);
})
.finally(function () {
// always do this
});

We can inject our file key within the URL and the Figma access token within the header “X-Figma-Token”. The empty constant postData is a placeholder for the data will will post to the API in order to create or update variables.

Read our tokens

We use Node.js file system (fs) to read files from our node_modules. The token build process creates these outputs, so we pull them in for our light and dark themes.

const filterThemeData = (theme, tokenType) => {
const fileDataLight = fs.readFileSync(
`./node_modules/@design-tokens/${theme}/dist/web/storybook/light.json`
);
const fileDataDark = fs.readFileSync(
`./node_modules/@design-tokens/${theme}/dist/web/storybook/dark.json`
);

const fileJSONLight = JSON.parse(fileDataLight);
const fileJSONDark = JSON.parse(fileDataDark);

/** filter then return the split data **/
};

Next we filter by token types (eg, color, radius, spacing, etc). We have data in each of our tokens that differentiate between primitives, semantics, and identify deprecated tokens. Custom filters eliminate deprecated tokens, token types that differ from which ones we want, as well as splitting our primitives from semantics.

  /** Specify only the primitives */
let primitives = filteredJSONLight.filter(function (token) {
return token.themeable === false;
});
/** Filter deprecated primitives */
primitives = primitives.filter((token) => {
if(token.comment) {
if (filterComments(token.comment)) return token
} else return token
})

/** Specify semantics for light and dark themes **/
const semanticLight = filteredJSONLight.filter(function (token) {
return token.themeable === true && token.comment ? filterComments(token.comment) : null;
});
const semanticDark = filteredJSONDark.filter(function (token) {
return token.themeable === true && token.comment ? filterComments(token.comment) : null;
});

return {
primitives: primitives,
semanticLight: semanticLight,
semanticDark: semanticDark,
};

This function gives us a way to filter the tokens into only the types and values we want to publish to Figma.

Understanding the POST format

The important part of this process is ensuring we reformat our tokens into a data model that Figma uses for API calls. Full documentation for this can be found on Figma developer resources. The basic call structure currently looks like this:

{
"variableCollections": [ ], // Each collection
"variableModes": [ ], // Modes from all collections
"variables": [ ], // Variables from all collections
"variableModeValues": [ ] // Values for every variable by mode
}

For every variable collection, we have the following data:

{
"action": "", // CREATE or UPDATE if existing
"id": "", // Temporary ID we can use in our initial POST data
"name": "", // The name we want to give the collection
"initialModeId": // ID of the default mode
}

For each of our variable modes, we have the following data. An important note is that every variable collection has a pre-existing initial mode by default — meaning the first/initail mode of any collection must always use the “UPDATE” action (cannot be created):

{
"action": "", // CREATE or UPDATE if existing
"id": "", // Temporary ID we can use in our initial POST data
"name": "", // The name we want to give the mode
"variableCollectionId": "" // The ID of the collection the mode belongs to
},

Variables themselves are defined without any values, but have a few more pieces of data for us to configure:

{
"action": "", // CREATE or UPDATE if existing
"id": "", // Temporary ID we can use in initial POST data
"name": "", // Name of the variable, which can define folder structure
"variableCollectionId": "", // ID of the collection the variable belongs
"resolvedType": "", // Variable types of COLOR, STRING,
"scopes": [], // Where the variable can be used in Figma
"hiddenFromPublishing": bool,// If the variable should be hidden
"codeSyntax": { // Optional different names for the variable per platform
"WEB": "", // eg. my-token-name
"ANDROID": "", // eg. MY_TOKEN_NAME
"iOS": "" // eg. myTokenName
}
}

Creating variable data for a token

We needed a function that would convert our token data into the data format required for variablesand variableModeValuesdata. First being a definition of the token itself — its name(s), type, scopes, and which collection it will belong to.

After filtering our theme data, for each color mode (light/dark) we will loop through our tokens and push them to an empty array. For color tokens, it looks something like this:

const createTokenVariableData = (...) => {
let variables = [];
let variableModeValues = [];

tokens.forEach((token) => {
const variableId = token.name;

/** getToenDisplayName will convert the token name into Figma structured format
* for example, color.container.background.primary will be Container/Background/Primary **/
const variableName = getTokenDisplayName(token.name, tokenType);

/** getTokenValues will convert into RGBA object or a Figma Alias value format **/
const value = getTokenValues(token, tokenType, tokens, fileKey)

let variableData = {
action: 'CREATE',
id: variableId,
name: variableName,
variableCollectionId: collectionId,
resolvedType: 'COLOR',
scopes: 'ALL_SCOPES',
hiddenFromPublishing: hiddenFromPublishing,
codeSyntax: {
// Our tokens use the web-based name by default
WEB: token.name,
// Android and iOS follow basic formatting we can do inline
ANDROID: token.name.replace(/\-/ig, '_').toUpperCase(),
iOS: toCamelCase(token.name)
}
};
variables.push(variableData)

let variableModeValuesData = {
variableId: variableId,
modeId: `${theme}-${colorScheme}`,
value: value,
};
variableModeValues.push(variableModeValuesData);
})
}

I’ve over-simplified the code above because we have to create variableModeValues for both light and dark color schemes, but this gives you an idea of what this looks like.

Create the “Themes” collection

Next we need a function to create a collection for our themes. We start with some empty arrays for our variableCollections, variableModes, variables, and variableModeValues.

const createThemeCollection = (fileKey, themes, colorSchemes) => {
const variableCollections = [];
const variableModes = [];
const variables = [];
const variableModeValues = [];
};

Then we’ll specify the name of our collection, a temporary ID, and specify that the initial mode should be the light color scheme of the first theme we create. We will also specify an array of value types we want to generate tokens for within this collection, as product teams can theme their colors, border radius, and spacing tokens:

const createThemeCollection = (fileKey, themes, colorSchemes) => {
const variableCollections = [];
const variableModes = [];
const variables = [];
const variableModeValues = [];

const typesForCollection = ['color', 'radius', 'spacing']
const collectionName = 'Themes';
const collectionId = `themed_tokens`;
const initialModeId = `${themes[0]}_light`;

variableCollections.push({
action: 'CREATE',
id: collectionId,
name: collectionName,
initialModeId: initialModeId,
});
};

After this, we will loop through each theme to create variables, variableModes, and variableModeValues. For each light and dark color schemes, we create new modes and push them to the empty array. Then, for each value type (color, radius, spacing), we create token variable data and push both the variables and the variableModeValues to their respective arrays.

  themes.forEach((theme, i) => {
// Create light mode for each theme
variableModes.push({
action: i === 0 ? 'UPDATE' : 'CREATE', // If it's the first mode, set to UPDATE
id: `${theme}_light`,
name: `${capitalize(theme)} (light)`, // rename the variable mode
variableCollectionId: collectionId, // uses the temporary id of the variable collection
});

// Create dark mode for each theme
variableModes.push({
action: 'CREATE',
id: `${theme}_dark`,
name: `${capitalize(theme)} (dark)`, // rename the variable mode
variableCollectionId: collectionId, // uses the temporary id of the variable collection
});

// Create variable data for color, radius, and spacing tokens
typesForCollection.forEach((collectionType) => {
const semanticData = createTokenVariableData(...);

variables.push(...semanticData.variables);
variableModeValues.push(...semanticData.variableModeValues);
});
});

After this, we will structure all our data into the format expected by Figma’s REST API and then POST to Figma:

  let postData = {
variableCollections: variableCollections,
variableModes: variableModes,
variables: variables,
variableModeValues: variableModeValues
};

/** Post data to Figma Rest API */
postDataToFigma(fileKey, figmaAccessToken, postData);

The wrench in the process: Figma UIDs

After we POST to Figma’s REST API, Figma will send a response with an object tempToRealId. Upon POSTing our new variables, Figma automatically replaces any of the IDs that we’ve defined in our functions above.

Bit of a pain, but I recognize why they do this.

So what does this mean for us? You can only POST once with the IDs that we’ve defined above. Afterword, a token we’ve named will only be accessible via the Figma automatically generated UID.

In other words this: color-container-background-accent
is replaced with this: VariableID:739:501

In order to continue POSTing updates to Figma variables as we update our design tokens, we need to retain a mapping of our original token names to the new Figma UIDS. Thankfully this is straightforward. All we need to do is:

  1. Capture the API response with tempToRealId values
  2. Create a function to use in place of temporary IDs to check if the token already has a Figma ID in the file

Capturing tempToRealId values

In our Axios call, after we post the data, we capture the response with the temp to real ID mapping, and write it to a new file.

axios
.post(`https://api.figma.com/v1/files/${fileKey}/variables`, postData, {
headers: {
'X-Figma-Token': figmaAccessToken,
},
})
.then(function (response) {
// Here is where we want to capture the tempToRealIds
const tempToRealId = response.data.meta.tempIdToRealId;

// Then write a file with the response data
fs.writeFile(
`figma/variableIds_${fileKey}.json`,
JSON.stringify(tempToRealId, null, 2),
function (err) {
if (err) throw err;
}
})
.catch(function (error) {
// log any errors
console.log(error);
})
.finally(function () {
// always do this
});

The output from this will look something like this:

{
"color-data-negative_400": "VariableID:7260:144659",
"color-data-negative_300": "VariableID:7260:144658",
"color-data-negative_200": "VariableID:7260:144657",
"color-data-negative_100": "VariableID:7260:144656",
...
}

Creating a function to use Figma IDs if they already exist

Just to clarify, if we are posting updates to color-data-negative_100, but that token was already posted in the Figma file, we do not want to duplicate the variable — rather, we want to update the existing one. So, if the variable already exists, use the Figma ID, otherwise use the temporary ID (name of our token).

So we read the contents of the previously created tempToRealId mappings, and if there’s a match for the token we’re looking for (eg, color-data-negative_100), then it will return the Figma ID (eg. VariableID:7260:144656)

const matchTokenToId = (tokenName, fileKey) => {
if (fs.existsSync(`./figma/variableIds_${fileKey}.json`)) {
const file = fs.readFileSync(`./figma/variableIds_${fileKey}.json`);
const fileData = JSON.parse(file);
if (fileData[tokenName] || fileData[tokenName] !== undefined) return fileData[tokenName];
else return tokenName;
}
else return tokenName;
};

It’s important that this function needs to replace any instance in the code where you specify an ID, whether it’s for a variableCollection, variableMode, variable, or variableModeValue.

We also added a boolean function itemExistsInFigma based on the same idea, in order to dynamically apply the correct action for each item when creating the POST data. If the item exists, we set the action to 'UPDATE', but if it does not we set it to 'CREATE'.

Challenges moving forward

When I began this work, I was hoping to incorporate these scripts into a build pipeline for fully automated Figma variable updates directy from our design token repository. However, we quickly uncovered some challenges we need to further strategize over.

First, Figma’s API does not offer any solutions that would help with change management in this workflow. As soon as you POST variable updates, they exist within the main file — there’s no warning of the update and no messaging within Figma regarding what’s changed. Furthermore, there’s no stop-gap that would enable designers to govern the file. What would be nice to have from Figma is the ability to create new branches of a file via the API, so we can follow git-like workflows within our Figma files any time these new variables are posted.

Secondly, we need to strategize further regarding whether our Figma resources should always be up-to-date with the latest stable release, or how we could potentially support beta, experimental, or “canary” releases of our tokens within Figma. Since we support multiple brands and design system teams, we need flexibility for designers to work with their experimentations while they evolve their products’ experience.

Finally, not all of our consumers of the Intuit Design System want access to every theme. For example, TurboTax designers may only want to see the TurboTax theme. This is with good reason — if they only ever design within one theme, adding all of the others will overcomplicate their design experience. What would be nice to have from Figma is the ability to select which modes you (as a consumer of a variables library) want to pull into your file. Or, alternatively, some other method of scoping variable modes to specific organizations or teams.

Thank you for reading

This is certainly a complex process, but I hope that what I’ve learned and shared with you is helpful as you begin to use Figma variables.

--

--

Nate Baldwin

Designer on @Intuit’s design system, previously @Adobe Spectrum. Intensity curious about color, visual perception, and the systemization of design.