DEV Community

Alexander
Alexander

Posted on

Generating dynamic Tailwind configs from Figma variables without breaking CI

The missing class disaster

I was staring at our production error logs on a Friday afternoon. A silent deployment had just gone out. Every primary button on the dashboard was completely transparent. Users could not click anything. It was a complete disaster.

I checked the codebase. The React components still had className="bg-brand-blue-500". But that class was doing absolutely nothing. Tailwind simply ignored it.

Turns out a designer had renamed a colour variable in Figma earlier that morning. They changed brand-blue to core-blue to match a new naming convention. Our naive automated sync script did exactly what it was told to do. It grabbed the new JSON file from Figma. It overwrote our Tailwind config. The CI pipeline saw no syntax errors. The build passed perfectly.

But React was still looking for the old class name. Tailwind stripped out the missing class during the production build. The styles vanished.

We realised we had a massive blind spot. You cannot just blindly dump Figma variables into a Tailwind config. You need a safety net. You need to know if a design token rename is going to break your components before you merge the pull request.

Mapping W3C tokens to Tailwind safely

Figma exports variables in the W3C Design Token format. This is basically a massive nested JSON object. Tailwind expects a very specific flat object structure for its theme configuration.

Here is what Figma gives you.

{
  "core": {
    "blue": {
      "500": {
        "value": "#3b82f6",
        "type": "color"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is what Tailwind actually wants in your tailwind.config.js file.

module.exports = {
  theme: {
    colors: {
      core: {
        blue: {
          500: '#3b82f6'
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Our first script just recursively stripped out the value and type keys. It was a simple map function. But this is exactly what caused the bug. It had no concept of history. It did not know what tokens existed yesterday.

We needed a validation step. The script had to compare the new Figma JSON against the current Tailwind config. If a token was missing, the script had to scan the codebase to see if we were still using it.

Building a validation layer in Node

We decided to write a custom Node script. This script runs in GitHub Actions every time a new token payload arrives from Figma.

First we flatten the new Figma tokens into a list of Tailwind class names. We just join the object keys with hyphens.

const fs = require('fs');

function flattenTokens(obj, prefix = '') {
  let classes = [];
  for (const key in obj) {
    if (obj[key].value) {
      classes.push(`${prefix}${key}`);
    } else {
      const newPrefix = prefix ? `${prefix}-${key}-` : `${key}-`;
      classes = classes.concat(flattenTokens(obj[key], newPrefix));
    }
  }
  return classes;
}

const figmaJson = JSON.parse(fs.readFileSync('./tokens.json', 'utf8'));
const newTailwindClasses = flattenTokens(figmaJson);
Enter fullscreen mode Exit fullscreen mode

So basically this gives us an array like ['core-blue-500', 'core-blue-600'].

Next we do the exact same thing for our existing tailwind.config.js theme object. We compare the two arrays. We find out which classes were deleted.

const oldConfig = require('./tailwind.config.js');
const oldTailwindClasses = flattenTokens(oldConfig.theme.colors);

const deletedClasses = oldTailwindClasses.filter(
  className => !newTailwindClasses.includes(className)
);

if (deletedClasses.length > 0) {
  console.log('Warning: The following tokens were removed from Figma:');
  console.log(deletedClasses);
}
Enter fullscreen mode Exit fullscreen mode

This is where the magic happens. If deletedClasses is empty, we just write the new config and move on. But if a designer deleted or renamed a token, we stop. We take that array of deleted classes and we scan our React components.

Catching broken components before the merge

We use a simple regular expression to search our .tsx files. We are looking for any string that matches the deleted Tailwind classes. We want to fail the CI build if we find a match.

const path = require('path');

function scanDirectory(dir, deletedClasses) {
  let brokenFiles = [];
  const files = fs.readdirSync(dir);

  for (const file of files) {
    const fullPath = path.join(dir, file);
    const stat = fs.statSync(fullPath);

    if (stat.isDirectory()) {
      brokenFiles = brokenFiles.concat(scanDirectory(fullPath, deletedClasses));
    } else if (fullPath.endsWith('.tsx')) {
      const content = fs.readFileSync(fullPath, 'utf8');

      for (const deletedClass of deletedClasses) {
        const regex = new RegExp(`bg-${deletedClass}|text-${deletedClass}|border-${deletedClass}`);
        if (regex.test(content)) {
          brokenFiles.push({ file: fullPath, missingClass: deletedClass });
        }
      }
    }
  }

  return brokenFiles;
}

const errors = scanDirectory('./src/components', deletedClasses);

if (errors.length > 0) {
  console.error('Build failed. The following files use deleted Figma tokens:');
  errors.forEach(err => console.error(`${err.file} depends on ${err.missingClass}`));
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

This tiny Node script completely changed how we handle design updates.

When the designer renames brand-blue to core-blue now, the GitHub Action runs. It notices brand-blue-500 is missing from the new JSON. It scans the src/components folder. It finds Button.tsx is still using bg-brand-blue-500. The script exits with a status code of 1. The pull request fails.

The developer gets a clear error message in GitHub. They know exactly which file to update. They change the class to bg-core-blue-500 in the React component. They push the commit. The pull request passes. The deployment goes out safely.

A packaged way to handle token drift

Writing custom regex parsers and AST scanners for your codebase is fun. I honestly enjoy building these little Node tools. But it takes time to maintain them. You have to handle edge cases for hover states. You have to handle responsive prefixes. It gets complicated really fast.

I ended up building a tool to handle this entire pipeline automatically. It is called Design System Sync. I created it because I was tired of writing the same W3C token parsers for every new project.

It is a Figma plugin that exports your variables directly to GitHub or Bitbucket. It automatically formats everything for Tailwind or plain CSS. It creates the pull request for you. It even generates visual diffs so you can see exactly which colours changed before you merge anything. It handles all the multi-mode variable exports for light and dark themes out of the box.

If you want to stop breaking staging environments with missing classes, 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.

You really need a safety net between Figma and your frontend code. Designers should be able to rename things. Developers should be able to merge things confidently. A solid validation pipeline makes both of those things possible.

Top comments (0)