The rogue grey incident
I was reviewing a pull request for a new onboarding flow last Thursday. The code looked perfectly fine at first glance. The logic was solid and the tests were passing. But then I looked at the CSS-in-JS styles. Someone had written color: '#4B5563' on a subheader component.
That specific hex code is not in our design system. Our official slate grey is #475569. The developer had just used a colour picker on a mockup and pasted the result directly into the codebase.
This happens all the time. Designers tweak a colour slightly in Figma. Developers inspect it and hardcode the value. Slowly but surely your codebase fills up with fifty shades of grey. You end up with a massive technical debt problem. Your design system becomes a suggestion instead of a rule.
We needed a way to stop this at the gate. We needed our continuous integration pipeline to catch raw hex codes and fail the build if they did not match our official tokens.
Here is exactly how we built a token drift detector using TypeScript and GitHub Actions.
Parsing components for raw values
Regular expressions are not smart enough to catch every hardcoded colour. You might have hex codes in string literals, template strings, or object properties. You need to parse the Abstract Syntax Tree of your files to find them reliably.
We wrote a small Node script using the TypeScript compiler API. It scans all our React components and looks for string literals that match a hex code pattern.
import * as ts from 'typescript'
import { readFileSync } from 'fs'
import { globSync } from 'glob'
const HEX_REGEX = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/
function findHardcodedColours(node: ts.Node, sourceFile: ts.SourceFile, found: any[]) {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
const text = node.text
if (HEX_REGEX.test(text)) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
found.push({
value: text,
line: line + 1,
character: character + 1,
file: sourceFile.fileName
})
}
}
ts.forEachChild(node, child => {
findHardcodedColours(child, sourceFile, found)
})
}
const files = globSync('src/components/**/*.tsx')
const violations = []
files.forEach(file => {
const source = readFileSync(file, 'utf8')
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true)
findHardcodedColours(sourceFile, sourceFile, violations)
})
console.log(JSON.stringify(violations, null, 2))
This script is fast and catches every single hex string in the codebase. But finding hex codes is only half the battle. We also need to know if those hex codes are actually valid tokens.
Comparing against the dictionary
We keep our design tokens in a standard JSON format. The next step is to load that dictionary and build a map of valid colour values. If a developer hardcodes a hex value that exactly matches a token we want to suggest they use the token name instead. If the hex value is completely unknown we want to throw a hard error.
import tokens from '../tokens.json'
function buildTokenMap(obj: any, prefix = ''): Record<string, string> {
let map: Record<string, string> = {}
for (const key in obj) {
if (obj[key].value) {
const tokenName = prefix ? `${prefix}.${key}` : key
map[obj[key].value.toLowerCase()] = tokenName
} else if (typeof obj[key] === 'object') {
const newPrefix = prefix ? `${prefix}.${key}` : key
const nested = buildTokenMap(obj[key], newPrefix)
map = { ...map, ...nested }
}
}
return map
}
const validTokens = buildTokenMap(tokens.colors)
violations.forEach(violation => {
const lowerHex = violation.value.toLowerCase()
const matchingToken = validTokens[lowerHex]
if (matchingToken) {
console.warn(`Warning: Use token '${matchingToken}' instead of ${violation.value} in ${violation.file}:${violation.line}`)
} else {
console.error(`Error: Unknown colour ${violation.value} found in ${violation.file}:${violation.line}`)
process.exitCode = 1
}
})
This script now does something incredibly useful. It transforms a silent design drift into a loud terminal error. But we want this to happen automatically on every pull request.
Annotating the pull request
GitHub Actions allow you to create custom annotations. These are the little error messages that show up directly inline on the code diff. We can format our script output to trigger these annotations automatically.
GitHub looks for a very specific string format in the standard output. It looks like ::error file={name},line={line},col={col}::{message}. We can update our script to log this exact format.
violations.forEach(violation => {
const lowerHex = violation.value.toLowerCase()
const matchingToken = validTokens[lowerHex]
if (matchingToken) {
console.log(`::warning file=${violation.file},line=${violation.line},col=${violation.character}::Hardcoded colour found. Please use the design token '${matchingToken}' instead of ${violation.value}.`)
} else {
console.log(`::error file=${violation.file},line=${violation.line},col=${violation.character}::Rogue colour detected. ${violation.value} is not in the design system. Check Figma for the correct token.`)
process.exitCode = 1
}
})
Now we just need to wire this up in a YAML workflow file. We want this to run whenever a developer opens or updates a pull request.
name: Token Drift Detector
on:
pull_request:
paths:
- 'src/components/**/*.tsx'
- 'src/components/**/*.ts'
jobs:
detect-rogue-colours:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run AST Token Checker
run: npx ts-node scripts/check-tokens.ts
When a developer pushes code with a random hex value the CI pipeline fails. An error pops up right on the specific line of code in the GitHub UI. It tells them exactly what they did wrong and how to fix it. This completely eliminated the manual back and forth in our code reviews.
Automating the source of truth
This entire pipeline relies on one crucial thing. The tokens.json file in your repository must be perfectly in sync with your Figma files. If a designer adds a new primary colour in Figma and the developer tries to use it the build will fail unless the JSON file is updated first.
Manual handoffs for these JSON files are a nightmare. People forget to export them. They copy and paste the wrong versions into Slack. The CI pipeline fails because the tokens dictionary is stale.
I actually built a tool to solve this exact problem. I created a plugin called Design System Sync. It exports your Figma design tokens directly to GitHub or Bitbucket via automatic pull requests. As soon as a designer updates a colour in Figma they can push a button to generate a PR with the new W3C formatted tokens.
The plugin handles CSS Variables and Style Dictionary formats natively. It even includes AI code generation for React or HTML. You can check it out at ds-sync.netlify.app or grab it directly from the Figma Community.
So basically by combining a strict CI check with automated Figma syncs you create a closed loop. Designers control the tokens in Figma. Developers are forced to use them in code. The CI pipeline enforces the rules without anyone having to be the bad guy in a pull request review.
Top comments (0)