You are looking at a pull request with over six hundred lines of hardcoded hex codes in Tailwind arbitrary values. Someone actually wrote bg-[#1a56db] across fifty different React components. The codebase is a mess of magic numbers. The design team just published a beautifully structured semantic colour system in Figma. They gave you a JSON file full of nested objects like surface.primary.hover. You need to get these into Tailwind somehow without breaking everything.
Standard Tailwind colours are brilliant for prototyping. They become a massive headache when you need to maintain a strict brand identity across a large product. You cannot ask developers to remember which shade of blue maps to a primary button state. Semantic tokens solve this by naming colours after their intent rather than their appearance.
We are going to write a small Node script to fix this. It will take a raw token file and map it directly into a Tailwind configuration.
Prerequisites
You will need Node installed on your machine. You also need a web project already set up with Tailwind CSS. Finally you need a raw JSON file containing your design tokens. Most design tools can export this format natively or via a community tool.
The Raw Token Data
Let us look at the raw data first. Most modern design tools spit out tokens in the W3C draft format. It looks quite heavy at first glance. It uses a deeply nested structure to organise the design decisions.
Create a file called tokens.json in your project root and add this data.
{
"color": {
"surface": {
"primary": {
"default": { "$value": "#1a56db", "$type": "color" },
"hover": { "$value": "#1e40af", "$type": "color" }
},
"danger": {
"default": { "$value": "#dc2626", "$type": "color" },
"hover": { "$value": "#b91c1c", "$type": "color" }
}
},
"text": {
"on-primary": { "$value": "#ffffff", "$type": "color" },
"critical": { "$value": "#991b1b", "$type": "color" }
}
}
}
This structure is great for machines but terrible for Tailwind. Tailwind expects a flat object or a gently nested one for its colour configuration. We need to bridge the gap.
The Transformation Script
Tbh nobody wants to map these by hand every time a hex code changes. We will write a small Node script to automate the boring part. This script reads the W3C format and flattens it into a structure Tailwind can actually understand.
Create a file called build-theme.js in your project root.
const fs = require('node:fs')
const rawData = fs.readFileSync('./tokens.json', 'utf8')
const tokens = JSON.parse(rawData)
function flattenTokens(obj, prefix = '') {
let result = {}
for (const key in obj) {
const value = obj[key]
let newKey = prefix ? `${prefix}-${key}` : key
if (newKey.endsWith('-default')) {
newKey = newKey.replace('-default', '')
}
if (value.$value) {
result[newKey] = value.$value
} else if (typeof value === 'object') {
const nested = flattenTokens(value, newKey)
result = { ...result, ...nested }
}
}
return result
}
const tailwindColors = flattenTokens(tokens.color)
fs.writeFileSync(
'./tailwind-theme.json',
JSON.stringify(tailwindColors, null, 2)
)
console.log('Successfully generated Tailwind theme.')
Let us break down what is happening here. The script reads the JSON file and passes the colour object into a recursive function. The function walks through every nested level. It builds a string key by combining the parent names.
Notice the special handling for the word default. In Figma you might name a token surface.primary.default. In Tailwind you just want to write bg-surface-primary. The script strips out the default suffix automatically to keep your classes clean.
Run this script in your terminal.
node build-theme.js
Plugging it into Tailwind
Now we have a clean JSON file sitting in our project. We just need to tell Tailwind to use it. Open your tailwind.config.js file. We will require the generated JSON and spread it into the extend block of our theme.
/** @type {import('tailwindcss').Config} */
const customTheme = require('./tailwind-theme.json')
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {
colors: customTheme
}
},
plugins: []
}
By placing the custom colours inside the extend block we keep all the default Tailwind colours available. This is usually the safest approach. If you want to strictly enforce only your semantic colours you would move the customTheme object up one level to completely overwrite the default palette.
What the output looks like
When you run your build process Tailwind picks up these new semantic colours. You can now use them exactly like the built-in utility classes.
<div class="p-6 bg-surface-primary hover:bg-surface-primary-hover rounded-lg">
<h2 class="text-text-on-primary font-bold text-xl">
Semantic colours are brilliant
</h2>
<button class="mt-4 bg-surface-danger hover:bg-surface-danger-hover text-text-on-primary px-4 py-2 rounded">
Delete Project
</button>
</div>
This is so much better than arbitrary values. Your team can read the classes and immediately understand the intent behind the styling. If the design team decides the primary surface needs to be slightly darker next month you just replace the token file and rerun the script. Not a single React component needs to change.
The best part is how this scales. When your design system grows to include borders and shadows you can update the script to parse those token types too. You just map them to their corresponding Tailwind configuration keys.
Honestly I got really tired of writing and maintaining these parser scripts for every new project. It felt like solving the exact same problem over and over again. So I built a tool called Design System Sync. It connects your Figma variables directly to your code repository and generates the exact formats you need automatically. It handles the Tailwind mapping right out of the box so you never have to write another recursive parser function. You can check it out at https://ds-sync.netlify.app?utm_source=devto&utm_medium=post&utm_campaign=bot or find it on the Figma Community here: https://www.figma.com/community/plugin/1561389071519901700?utm_source=devto&utm_medium=post&utm_campaign=bot. It creates pull requests with visual diffs whenever a designer updates a colour.
Top comments (1)
How do you handle token updates when the design system changes, do you re-generate the entire theme or is there a more incremental approach? I'd love to swap ideas on this.