DEV Community

Alexander
Alexander

Posted on

Auto-syncing Tailwind config from Figma variables on every pull request

The merge conflict that broke staging

I opened GitHub last Tuesday and saw a massive red error on our main deployment pipeline. Staging was completely broken. Two different frontend teams had tried to update our Tailwind configuration file at the exact same time.

Team A added new spacing utilities for a marketing landing page. Team B added custom brand colours for a new product dashboard. Git got confused during the merge. Someone resolved the conflict manually and force-pushed the changes. We lost half the core brand colours and production deployments ground to a halt.

This happens all the time when a single configuration file becomes a bottleneck. Developers manually copy hex codes and pixel values from Figma. They paste them into a massive JavaScript object. Someone makes a typo. Someone else accidentally deletes a bracket. It is a fragile process.

I realised we needed to remove humans from this specific loop. The source of truth for design decisions is Figma. The destination is Tailwind. The bridge between them should be code.

The structure of a proper token file

We decided to use the W3C Design Token format. It is a standard JSON structure that represents design decisions. It keeps everything predictable.

Here is what a raw token file looks like when exported properly.

{
  "colors": {
    "brand": {
      "primary": {
        "$value": "#2563eb",
        "$type": "color"
      },
      "secondary": {
        "$value": "#4f46e5",
        "$type": "color"
      }
    },
    "surface": {
      "background": {
        "$value": "#ffffff",
        "$type": "color"
      }
    }
  },
  "spacing": {
    "sm": {
      "$value": "0.5rem",
      "$type": "dimension"
    },
    "md": {
      "$value": "1rem",
      "$type": "dimension"
    },
    "lg": {
      "$value": "1.5rem",
      "$type": "dimension"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice how clean that is. Every token has a value and a explicit type. We just need to transform this nested JSON into a flat object that Tailwind understands.

Writing the conversion script

We wrote a small TypeScript utility to handle the translation. We wanted it to run automatically in our CI pipeline. The script reads the JSON tokens and outputs a valid Tailwind preset.

Here is the exact code we use. I kept it simple and focused.

import fs from 'fs'
import path from 'path'

interface TokenNode {
  $value?: string
  $type?: string
  [key: string]: any
}

function flattenTokens(obj: TokenNode, prefix = ''): Record<string, string> {
  let result: Record<string, string> = {}

  for (const key in obj) {
    if (key.startsWith('$')) continue

    const node = obj[key]
    const newPrefix = prefix ? `${prefix}-${key}` : key

    if (node.$value !== undefined) {
      result[newPrefix] = node.$value
    } else if (typeof node === 'object') {
      const nested = flattenTokens(node, newPrefix)
      result = { ...result, ...nested }
    }
  }

  return result
}

function generateTailwindTheme() {
  const rawData = fs.readFileSync(path.join(__dirname, 'tokens.json'), 'utf8')
  const tokens = JSON.parse(rawData)

  const colors = flattenTokens(tokens.colors || {})
  const spacing = flattenTokens(tokens.spacing || {})

  const tailwindConfig = {
    theme: {
      extend: {
        colors,
        spacing
      }
    }
  }

  const fileContent = `export default ${JSON.stringify(tailwindConfig, null, 2)}`
  fs.writeFileSync(path.join(__dirname, 'tailwind.preset.js'), fileContent)
  console.log('Tailwind preset generated successfully')
}

generateTailwindTheme()
Enter fullscreen mode Exit fullscreen mode

This script recurses through the W3C token tree. It ignores the metadata keys that start with a dollar sign. It builds flat strings like brand-primary and assigns the hex code. Finally it writes a complete JavaScript module to the disk.

You do not need to touch your main Tailwind config anymore. You just import this generated preset.

import designSystemPreset from './tailwind.preset.js'

export default {
  presets: [designSystemPreset],
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Setting up the GitHub Action

A script is useless if you have to remember to run it. We needed this to happen automatically whenever design tokens changed.

We set up a GitHub Action. It listens for changes to our token file. When a pull request updates the JSON data the action runs our conversion script. It then commits the updated Tailwind preset right back to the same pull request.

Here is the workflow file we built.

name: Sync Tailwind Preset

on:
  pull_request:
    paths:
      - 'tokens.json'

jobs:
  build-preset:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          ref: ${{ github.head_ref }}

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Generate Preset
        run: npx ts-node scripts/generate-preset.ts

      - name: Commit changes
        run: |
          git config --global user.name 'github-actions[bot]'
          git config --global user.email 'github-actions[bot]@users.noreply.github.com'
          git add tailwind.preset.js
          git commit -m "chore: update tailwind preset from tokens" || echo "No changes to commit"
          git push
Enter fullscreen mode Exit fullscreen mode

Now the process is entirely hands off. A designer updates a variable in Figma. A pull request is created with the new JSON. GitHub Actions spots the change and regenerates the Tailwind preset. The developers just review the code and click merge. No more merge conflicts. No more manual data entry.

Letting designers own the source of truth

Building this pipeline from scratch was a fun weekend project. But maintaining custom conversion scripts gets tedious when you start dealing with complex typography rules or multiple dark mode themes.

That is exactly why I built Design System Sync. It handles this entire workflow out of the box. You run the plugin inside Figma. It exports your variables and styles directly to GitHub or Bitbucket. It automatically creates the pull request with a visual diff. It supports W3C Design Tokens and CSS Variables natively.

If you want to skip writing custom token parsers you can check out the product page 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. The free tier gives you five exports a month which is plenty for testing out a new workflow.

Stop manually copying hex codes. Let the machines do the boring work so you can focus on building actual features.

Top comments (0)