A PR review on a Tuesday morning caught my eye. A developer had hardcoded a hex value into a custom utility class. I left a comment asking why they did not just use our standard brand token. They replied that their editor autocomplete did not show it.
I checked the main branch. It turns out the design team had updated the token names in Figma three weeks prior. They renamed a bunch of nested colour groups. Nobody had updated the Tailwind config to match.
This happens all the time. Designers update variables in Figma. Developers do not realise the names changed. Autocomplete fails. Developers hardcode values to hit their deadlines. Your design system slowly falls apart.
We needed to fix this at the source. The goal was simple. We wanted to take raw Figma variables and automatically generate a fully typed Tailwind configuration on every PR.
Reading the raw W3C format
Figma variables export as a JSON file. The industry is slowly moving towards the W3C Design Token format. This is a nested JSON structure where actual values are denoted by a $value key.
You might get a file that looks like this from your design team.
{
"color": {
"brand": {
"primary": {
"$type": "color",
"$value": "#0F172A"
},
"secondary": {
"$type": "color",
"$value": "#334155"
}
}
},
"spacing": {
"small": {
"$type": "dimension",
"$value": "0.5rem"
}
}
}
This is great for machines. It is terrible for Tailwind. Tailwind expects a flat or slightly nested object of strings. It does not know what to do with $value or $type keys. We needed a script to flatten this out.
Writing the transformation script
I decided to write a small Node script in TypeScript. Its only job is to read that JSON file and recursively strip out the W3C metadata. We want a clean object that perfectly matches the Tailwind theme specification.
Here is the recursive function we built to handle the flattening. Notice how it checks for the $value key to determine if it has reached a leaf node.
import fs from 'fs'
import path from 'path'
interface TokenNode {
$value?: string
$type?: string
[key: string]: any
}
function flattenTokens(obj: TokenNode): Record<string, any> {
const result: Record<string, any> = {}
for (const key in obj) {
const node = obj[key]
if (node && typeof node === 'object') {
if ('$value' in node) {
result[key] = node.$value
} else {
result[key] = flattenTokens(node)
}
}
}
return result
}
This function is small but it does exactly what we need. It walks through the nested Figma export. When it finds a $value property it grabs the string. Otherwise it keeps going deeper.
We can then map the top level keys to the specific Tailwind theme sections.
function generateTheme(rawJson: Record<string, any>) {
const flattened = flattenTokens(rawJson)
return {
colors: flattened.color || {},
spacing: flattened.spacing || {},
borderRadius: flattened.radius || {}
}
}
Creating the typed configuration file
Just having the JavaScript object is not enough. We need to actually write this to a file that Tailwind can consume. We also want it to be a TypeScript file. This ensures our developers get that sweet editor autocomplete.
The easiest way to do this is to stringify the generated theme object and inject it into a template.
function writeTailwindConfig(theme: Record<string, any>) {
const themeString = JSON.stringify(theme, null, 2)
const fileContent = `
import type { Config } from 'tailwindcss'
const generatedTheme = ${themeString}
const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: generatedTheme
},
plugins: [],
}
export default config
`.trim()
fs.writeFileSync(
path.resolve(process.cwd(), 'tailwind.config.ts'),
fileContent
)
}
This script reads the Figma JSON and writes a brand new tailwind.config.ts file into the root of our project. The Config type from Tailwind does a lot of heavy lifting here. It makes sure our generated object conforms to expected types.
Running it in CI
Local scripts are fine. Honestly though people forget to run them. If a designer drops a new JSON file into the repository we want the Tailwind config to update automatically.
We wired this up using GitHub Actions. The workflow listens for changes to our tokens.json file. When it detects a change it runs our transformation script and commits the updated config file back to the branch.
Here is the YAML file we use for this pipeline.
name: Sync Tailwind Config
on:
push:
paths:
- 'design-system/tokens.json'
jobs:
build-config:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run transformation script
run: npx ts-node scripts/transform-tokens.ts
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "chore: update tailwind config from tokens"
file_pattern: "tailwind.config.ts"
Now the process is completely hands off. A designer updates variables in Figma and exports the JSON. They make a PR with the new JSON file. GitHub Actions sees the change and runs the script. The script generates a fresh tailwind.config.ts file. The action commits that file right back to the designer's PR.
When a developer pulls the branch down their editor instantly knows about the new colours. Autocomplete works perfectly. Nobody hardcodes hex values anymore.
Skipping the manual pipeline
Building this pipeline taught me a lot about how W3C tokens map to utility classes. Tbh maintaining custom transformation scripts across multiple projects gets tedious. You end up copying and pasting the same build scripts everywhere. You also have to handle edge cases like typography ramps and complex shadow arrays.
This exact pain point is why I built Design System Sync. It handles the entire pipeline for you. It exports your Figma variables directly to GitHub or Bitbucket via automatic PRs. It also translates the raw variables into whatever format you actually need. You can get standard W3C JSON or you can have it output perfectly formatted CSS Variables and Tailwind configs.
If you want to stop writing custom token parsers 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.
Setting up a solid bridge between your design tools and your code base takes a bit of work upfront. It is totally worth it. Your developers get reliable autocomplete. Your designers get the confidence that their changes actually ship. And you never have to argue about hardcoded hex values in a PR review again.
Top comments (0)