Syncing Figma Variables to CSS Variables
Would you like to keep your design tokens in sync between Figma and code? I thought I'd share some of my experiences doing so over the years and talk through it from a high level. This isn't specific to any single company, and every company configures their tokens in Figma differently. The Figma APIs and some JavaScript/TypeScript are all you need to make this syncing a reality.
This article originated from a post I did over on Bluesky. David Darnes suggested I write a blog post about it, so here we are (thank you for the suggestion!)!
Generating your CSS variables from Figma variables ensures you're always in sync with design. Once you have it in place, you can run it via a cron job, or as a manual script. It removes the manual, laborious process of poking around Figma's UI and copy/pasting the variables into your CSS. Instead, run a script and let the computer do it! There's definitely an open source opportunity here to do a lot of the "token processing" for you. But we'll get into that another time.
Maybe you're using primitive and semantic tokens? Or maybe it's a wild west with how your variables are defined? No matter how things are structured, we can still map your tokens to CSS variables.
Fetching the Variables
To get the variables out of Figma, you can hit their REST API directly. I've been using the local
endpoint as described here to get all of the variables from a given file.
You can use the published variables instead if you like, and the response is slightly different. Local gives you both local variables and remote, so this normally works best for situations I've been in.
You can fetch the variables in node, but here's how you'd curl
it if you want to see what the response looks like. You pull the file ID from the URL.
curl -X GET \
https://api.figma.com/v1/files/YOUR_FILE_ID/variables/local\?scopes\=file_variables:read
-H 'X-FIGMA-TOKEN: YOUR_FIGMA_TOKEN'
You'll get back a big ol' JSON blob to parse through that is in the following shape.
{
"status": Number,
"error": Boolean,
"meta": {
"variables": {
[variableId: String]: {
"id": String,
"name": String,
"key": String,
"variableCollectionId": String,
"resolvedType": 'BOOLEAN' | 'FLOAT' | 'STRING' | 'COLOR'
"valuesByMode": {
[modeId: String]: Boolean | Number | String | Color | VariableAlias,
}
"remote": Boolean,
"description": String,
"hiddenFromPublishing": Boolean,
"scopes": VariableScope[],
"codeSyntax": VariableCodeSyntax,
}
},
"variableCollections": {
[variableCollectionId: String]: {
"id": String,
"name": String,
"key": String,
"modes": [
{
"modeId": String,
"name": String,
}
],
"defaultModeId": String,
"remote": Boolean,
"hiddenFromPublishing": Boolean,
"variableIds": String[],
"deletedButReferenced": Boolean,
}
}
}
}
}
Parsing the Data
Here is where you can put on your data processing / computer science hat. We need to associate the name of a variable with the raw CSS value by iterating through the keys in this large object.
variables
in this response are pretty self explanatory. They are the raw variables from Figma. This houses all of the CSS values you'll need.
variableCollections
on the other hand, you may not be familiar with as a developer.
A collection is a set of variables and modes. Collections can be used to organize related variables together. For example, use one collection to localize text in different languages, and another collection for spatial values.
The collection is specific to how your team organizes your variables. For example, maybe one collection is for colors related to data visualization, while another is for font sizes.
If you are using variable modes for light and dark themes, there will be a variable collection in here that will help you identify if a particular color in variables
belongs to "light" versus "dark" mode. It also lists all of the variable IDs that have those modes under variableIds
.
As you are parsing through this data, you'll want to keep track of what the name of the token is and the raw CSS value by iterating over these things. I like storing this information in a JavaScript object/JSON.
Here's a made up example of a variable collection from Figma's API supporting both light and dark modes.
"VariableCollection:some-long-id": {
"modes": [
{ "modeId": "1", "name": "Dark" },
{ "modeId": "2", "name": "Light" }
],
"variableIds": [
"VariableID:1",
"VariableID:2"
]
}
We've got two modes: one for "light" and one for "dark". You'll see there's also an array of variableIds
.
If we pull up VariableID:1
from that array, you'll see something like the following. Here we have a background/primary
semantic token that we'd like to export.
"VariableID:1": {
"id": "VariableID:1",
"name": "background/primary",
"valuesByMode": {
"1": { "type": "VARIABLE_ALIAS", "id": "VariableID:5" },
"2": { "type": "VARIABLE_ALIAS", "id": "VariableID:6" }
}
}
We can pull the name of the token from name
, but to get the value we need to look into valuesByMode
. Notice how it is "by mode" — yup, we've got two modes, a light and dark mode, so we have two valuesByMode
defined here.
The valuesByMode["1"]
key refers to the modeId
above, which is DARK
. The valuesByMode["2"]
key refers LIGHT
. So background/primary
has two values, one for when in light mode and another for dark mode.
Oh no! A VARIABLE_ALIAS
? What's that? It's a reference to yet another variable. Depending on how your designers have setup variables, you may have deeply nested references. In your data parsing, this complicates things slightly, as you don't have access directly to your color value. For this, you'll need to follow the VARIABLE_ALIAS
trail.
If we follow VariableID:5
, we get to our raw color value (in rgba). In this made up example, the Designer Tony has associated the gray-900
primitive token to background-primary
in the dark mode.
"VariableID:5": {
"id": "VariableID:5",
"name": "gray-900",
"valuesByMode": {
"random-id": {
"r": 15,
"g": 23,
"b": 42,
"a": 1
}
}
}
And the Light Mode definition would be defined as well. Designer Tony has associated the gray-100
primitive token to background-primary
in the light mode.
"VariableID:6": {
"id": "VariableID:6",
"name": "gray-100",
"resolvedType": "COLOR",
"valuesByMode": {
"random-id": {
"r": 241,
"g": 245,
"b": 249,
"a": 1
}
}
}
So that's following the design token trail all the way down from a semantic token to an alias to a primitive token. You'll need to keep track of how these semantic tokens reference the primitive ones. Once again, I typicaly store it in a JavaScript object or write it to JSON for convenience.
Now repeat this process for every token! Ah, recursion and loops. Gotta love it!
Tokens Checkpoint
After going through above, you'll have an object that looks something like the following.
{
"light": {
"background/primary": {
"value": "gray-100",
"type": "COLOR"
}
},
"dark": {
"background/primary": {
"value": "gray-900",
"type": "COLOR"
}
},
"primitive": {
"gray-100": {
"value": "rgb(241 245 249)",
"type": "COLOR"
},
"gray-900": {
"value": "rgb(15 23 42)",
"type": "COLOR"
}
}
}
Awesome! Now all of your Figma Variables are in a format you can use to generate your CSS variables from. Before doing that, let's discuss why you might want to adjust some of these value
s before pushing them over to CSS values.
Converting Values
You may notice that Figma provides rgba
for color values. You may want to use hex or something else. With the power of JavaScript, you can do whatever you want here! For the rest of the article, for simplicity, I'll keep rolling with rgb()
, but know that hex, oklch, or whatever you want to support is an option, you'll need to write the code to do these conversions.
When exporting non-color token values, the resolved type might be FLOAT
. A lot of times these are for pixel values. I prefer converting these values from pixels to rem
s for most cases and accessibility.
For the names of each token, you likely want to follow the CSS Variable syntax. So rather than background/primary
, you probably want --background-primary
instead. Maybe you even want to add a prefix in there to differentiate between Design System tokens versus application level tokens (e.g., --ds-background-primary
). This type of conversion can happen wherever you see fit. Either during the above process, or in the next step.
Create the CSS Variables
Now that you have all of your names and variables in a nice format, you can write them to CSS files directly.
Depending on how you want to structure your files, you could have separate primitive and semantic files.
/* primitive.css */
:root {
--gray-100: rgb(241 245 249);
--gray-900: rgb(15 23 42);
}
/* semantic-dark.css */
:root .dark {
--background-primary: var(--gray-900);
}
/* semantic-light.css */
:root .light {
--background-primary: var(--gray-100);
}
If you only want to expose your semantic tokens and not your primitive ones, you can skip over them completely by writing the raw primitive value instead.
/* light.css */
:root .light {
/* Value is derived from `--gray-100` */
--background-primary: rgb(241 245 249);
}
/* dark.css */
:root .dark {
/* Value is derived from `--gray-900` */
--background-primary: rgb(15 23 42);
}
Bridging Design and Engineering
With all of above in place, you've got all you need! Obviously I oversimplified a lot of this, but hopefully this gives you a starting point and a decent understanding to start diving in yourself.
Every use case is slightly different, so I mostly wanted to plant the seed that you can sync a lot of things from Figma and into code using their API. The Figma API is very powerful and probably underutilized with most teams.
Above I touched on an opportunity to handle some of this heavy lifting in open source by providing a package that'll fetch your variables and write them to a JSON object. Then you'd need to take that object and figure out how you'd want to structure your CSS files. I have nothing to share quite yet, but watch this space and something may pop up here soon™.
Overall, keeping Figma and code in sync can be a manual process. But it doesn't have to be! By keeping our Figma and CSS variables in sync, it ensures we continue to bridge the gap with our Design Systems between design and engineering. As always, thanks for reading. See you soon!
👋