I woke up to a sea of red CI failure notifications last Tuesday. A junior developer had tried to update our primary brand colour in the codebase. They manually copied the new hex code from Figma into our Tailwind config file. But they accidentally nested the new colour key under the wrong object level. This broke fifty different React components that relied on the bg-primary-500 utility class.
The fix took two minutes. The underlying problem took me a week to solve properly.
We were relying on humans to act as translation layers between our Figma variables and our code. Humans get tired. Humans make typos. We needed a machine to do this.
I decided to build a pipeline that automatically generates our Tailwind config directly from our Figma variables. Every time a designer updates a colour or a spacing unit in Figma, a GitHub Action catches the change and opens a pull request with the updated tailwind.config.js file.
This is exactly how we built it.
Getting the raw data out of Figma
The first step is getting the variable data out of Figma in a format we can actually use. Figma provides a REST API that lets you fetch local variables from a file.
You need a personal access token and your file key. You can find the file key in your Figma URL right after the figma.com/file/ part.
We wrote a simple Node script to pull this data.
const fetch = require('node-fetch')
const fs = require('fs')
const FIGMA_TOKEN = process.env.FIGMA_TOKEN
const FILE_KEY = process.env.FIGMA_FILE_KEY
async function fetchFigmaVariables() {
const response = await fetch(
`https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
{
headers: {
'X-Figma-Token': FIGMA_TOKEN
}
}
)
const data = await response.json()
fs.writeFileSync('./raw-figma-vars.json', JSON.stringify(data, null, 2))
console.log('Figma variables saved successfully.')
}
fetchFigmaVariables()
This gives you a massive JSON file. It contains a lot of metadata you do not care about. You will see collection IDs and mode IDs mixed in with the actual values. We need to filter this down to just the raw values.
Here is a simplified look at what the relevant part of that JSON structure looks like.
{
"meta": {
"variables": {
"VariableID:123": {
"name": "colors/primary/500",
"resolvedType": "COLOR",
"valuesByMode": {
"ModeID:456": {
"r": 0.1,
"g": 0.33,
"b": 0.85,
"a": 1
}
}
}
}
}
}
Notice how Figma stores colours as RGBA values between 0 and 1. Tailwind expects standard hex codes or CSS rgb strings. We have to convert this.
Parsing Figma JSON into a Tailwind theme
Now we need a script to read that raw JSON and spit out a valid Tailwind configuration object. We want to map the Figma variable names to Tailwind utility classes.
Our designers use a naming convention like colors/primary/500. We need to parse that into a nested object that Tailwind understands.
Here is the parser script we use to handle colours and spacing.
const fs = require('fs')
function rgbaToHex(r, g, b, a) {
const toHex = (value) => {
const hex = Math.round(value * 255).toString(16)
return hex.length === 1 ? '0' + hex : hex
}
if (a !== 1) {
const alpha = toHex(a)
return `#${toHex(r)}${toHex(g)}${toHex(b)}${alpha}`
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
function generateTailwindConfig() {
const rawData = JSON.parse(fs.readFileSync('./raw-figma-vars.json', 'utf8'))
const variables = rawData.meta.variables
const tailwindTheme = {
colors: {},
spacing: {}
}
Object.values(variables).forEach(variable => {
const nameParts = variable.name.split('/')
const category = nameParts[0]
const modeId = Object.keys(variable.valuesByMode)[0]
const rawValue = variable.valuesByMode[modeId]
if (category === 'colors' && variable.resolvedType === 'COLOR') {
const colorName = nameParts[1]
const shade = nameParts[2] || 'DEFAULT'
const hexValue = rgbaToHex(rawValue.r, rawValue.g, rawValue.b, rawValue.a)
if (!tailwindTheme.colors[colorName]) {
tailwindTheme.colors[colorName] = {}
}
tailwindTheme.colors[colorName][shade] = hexValue
}
if (category === 'spacing' && variable.resolvedType === 'FLOAT') {
const spacingName = nameParts[1]
tailwindTheme.spacing[spacingName] = `${rawValue}px`
}
})
const configContent = `
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: ${JSON.stringify(tailwindTheme, null, 2)}
},
plugins: [],
}
`
fs.writeFileSync('./tailwind.config.js', configContent)
console.log('Tailwind config generated successfully.')
}
generateTailwindConfig()
This script reads the raw data and builds a clean theme object. It handles the RGBA to hex conversion automatically. It also maps the nested Figma names into the exact structure Tailwind needs.
Automating the sync with GitHub Actions
Running scripts locally is fine for testing. But we want this to happen automatically. We need a GitHub Action that runs on a schedule or triggers via a webhook from Figma.
We set up a workflow that runs every night. It pulls the latest variables and generates the config. If there are changes, it opens a pull request.
Here is the workflow file we put in .github/workflows/sync-tokens.yml.
name: Sync Figma Variables to Tailwind
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install node-fetch
- name: Fetch Figma Variables
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
FIGMA_FILE_KEY: ${{ secrets.FIGMA_FILE_KEY }}
run: node scripts/fetch-figma.js
- name: Generate Tailwind Config
run: node scripts/generate-tailwind.js
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: sync tailwind config with figma variables"
title: "Design System Update: Tailwind Config"
body: "Automated PR to sync Tailwind config with the latest Figma variables."
branch: "update-design-tokens"
base: "main"
This workflow is entirely hands-off. The designers update a hex code in Figma. The next morning there is a PR waiting for review. The developers just check the diff and hit merge. Nobody copies and pastes hex codes anymore.
Skipping the custom scripts entirely
Building this pipeline taught me a lot about the Figma API. But maintaining custom parser scripts can get annoying. You have to update your regex every time a designer decides to use a new naming convention.
I actually built a tool to solve this exact problem without the custom code. I created a plugin called Design System Sync. It exports Figma variables directly to GitHub or Bitbucket via automatic pull requests.
It handles all the format conversions for you. You can export W3C Design Tokens or raw CSS Variables. It also handles change detection with visual diffs so you can see exactly what changed before you merge.
If you are tired of manually updating your configs, you can check out the website at https://ds-sync.netlify.app?utm_source=devto&utm_medium=post&utm_campaign=bot. You can also grab the plugin directly from the Figma Community here: https://www.figma.com/community/plugin/1561389071519901700?utm_source=devto&utm_medium=post&utm_campaign=bot.
Automating this handoff is one of the highest return investments you can make for your team. It completely eliminates a whole category of visual bugs. Stop pasting hex codes. Let the machines do the translation.
Top comments (0)