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.

Figma's docs

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 values 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 rems 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!

Update Nov 21,2024

Yonatan Kra over on Bluesky mentioned:

Have you thought of converting the figma output to a json and then using style dictionary to convert it to css? It has the added benefit of then being able to convert it to multiple formats (like native mobile stuff if your DS needs to support that) or multiple theme versions. WDYT?

Absolutely! I actually had written this in the initial article but ended up removing it to keep this scoped to CSS variables. You could write these Figma variables to Style Dictionary and then get multi-platform design tokens. This could be huge for some Design System teams.

More from David Darnes as well

I suppose vanilla js would be more performant, but Style Dictionary means you can leverage their well documented API. Plus as Tony said you can generate all distribution formats. In the past we've done that and wrapped it into a single npm package

Style Dictionary would also allow you to create a Tailwind config, so that you can use all of your tokens with TailwindCSS. Style Dictionary opens the door to a lot of great ideas if you want more than just CSS variables!


👋