Design tokens solve the consistency problem—but only if they stay synchronized between design tools and production code. The moment designers update a color palette in Figma while developers reference a stale token file, the system breaks down.
Manual synchronization doesn't scale. Designers can't be expected to export tokens and notify developers every time a value changes. Developers can't be expected to manually update token files every time a design refinement happens.
Automation is the only viable solution. This guide builds a complete automated workflow that syncs tokens from design tools to code repositories to production environments—no manual intervention required.
The Token Synchronization Problem
Most teams have experienced this sequence:
- Designer updates primary button color from
#2563ebto#3b82f6in Figma - Developer builds new feature referencing old token values
- Design QA catches the inconsistency three days later
- Developer updates token file manually
- Components using old hardcoded values remain inconsistent
- Cycle repeats next sprint
The failure points are predictable:
No change notification. Developers don't know when design updates happen unless designers manually communicate them.
No automated sync. Getting updated tokens from Figma to code requires manual export and file replacement.
No enforcement. Nothing prevents developers from using hardcoded values instead of token references.
No validation. Merged code might reference tokens that don't exist or use values that don't match design.
An automated workflow eliminates all four failure points.
Architecture Overview
The complete automation pipeline has five stages:
[Design Tool] → [Token Export] → [Transform] → [Code Repository] → [Production]
↓ ↓ ↓ ↓ ↓
Figma API/Plugin Style Dict. Git + CI Deployment
Sketch Tokens Studio Custom script GitHub Actions Vercel
Penpot Manual export Build process GitLab CI Netlify
Each stage handles a specific transformation:
- Design tool stores the source of truth
- Export mechanism pulls tokens from design tool
- Transform layer converts design formats to code formats
- Repository stores versioned token files
- Production consumes tokens in deployed applications
Let's build each stage.
Stage 1: Setting Up Token Export from Figma
Figma is the most common design tool, so we'll start there. The pattern applies to other tools with minor adjustments.
Option A: Figma Tokens (Tokens Studio)
Install the Tokens Studio plugin in Figma.
1. Structure tokens in Figma using the plugin UI:
Create token sets for each category:
core/color- base palettecore/spacing- spacing scalecore/typography- font sizes, weights, line heightscore/radius- border radius valuessemantic/color- semantic color mappingssemantic/component- component-specific tokens
2. Use token aliasing for semantic tokens:
{
"core": {
"color": {
"blue": {
"50": { "value": "#eff6ff", "type": "color" },
"500": { "value": "#3b82f6", "type": "color" },
"700": { "value": "#1d4ed8", "type": "color" }
}
}
},
"semantic": {
"color": {
"action": {
"primary": {
"value": "{core.color.blue.500}",
"type": "color"
}
}
}
}
}
3. Sync to GitHub directly from plugin:
Tokens Studio supports GitHub sync. Configure:
- Repository:
your-org/your-repo - Branch:
design-tokens - Path:
design-tokens/tokens.json - Personal access token (with
reposcope)
Enable "Sync on save" so every token update commits to GitHub automatically.
Option B: Figma API Direct Sync
If you can't use plugins or need more control, sync via Figma API:
// scripts/sync-figma-tokens.ts
import fetch from 'node-fetch';
import { writeFileSync } from 'fs';
const FIGMA_FILE_KEY = process.env.FIGMA_FILE_KEY!;
const FIGMA_TOKEN = process.env.FIGMA_TOKEN!;
async function fetchFigmaFile() {
const response = await fetch(
`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}`,
{
headers: {
'X-Figma-Token': FIGMA_TOKEN,
},
}
);
return response.json();
}
function extractColorTokens(figmaFile: any) {
const colorStyles = figmaFile.styles;
const tokens: Record<string, string> = {};
for (const [key, style] of Object.entries(colorStyles)) {
if (style.styleType === 'FILL') {
const fills = style.fills;
if (fills[0]?.type === 'SOLID') {
const { r, g, b, a } = fills[0].color;
const hex = rgbaToHex(r, g, b, a);
tokens[style.name.replace(/\//g, '.')] = hex;
}
}
}
return tokens;
}
function rgbaToHex(r: number, g: number, b: number, a: number = 1): string {
const toHex = (n: number) => {
const hex = Math.round(n * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
return a < 1 ? hex + toHex(a) : hex;
}
async function main() {
console.log('Fetching Figma file...');
const figmaFile = await fetchFigmaFile();
console.log('Extracting tokens...');
const tokens = {
color: extractColorTokens(figmaFile),
};
console.log('Writing tokens...');
writeFileSync(
'./design-tokens/figma-raw.json',
JSON.stringify(tokens, null, 2)
);
console.log('✓ Sync complete');
}
main();
Run this on a schedule (GitHub Actions cron) to poll Figma periodically.
Stage 2: Transforming Tokens to Code Formats
Raw Figma tokens need transformation into formats code can consume. Style Dictionary is the industry standard tool.
Install dependencies:
npm install -D style-dictionary @tokens-studio/sd-transforms
Configure Style Dictionary:
// style-dictionary.config.js
const StyleDictionary = require('style-dictionary');
const { registerTransforms } = require('@tokens-studio/sd-transforms');
registerTransforms(StyleDictionary);
module.exports = {
source: ['design-tokens/tokens.json'],
platforms: {
// CSS custom properties
css: {
transformGroup: 'tokens-studio',
buildPath: 'styles/',
files: [
{
destination: 'tokens.css',
format: 'css/variables',
options: {
outputReferences: true,
},
},
],
},
// TypeScript
ts: {
transformGroup: 'tokens-studio',
buildPath: 'src/tokens/',
files: [
{
destination: 'index.ts',
format: 'typescript/es6-declarations',
},
],
},
// JSON for runtime consumption
json: {
transformGroup: 'tokens-studio',
buildPath: 'public/',
files: [
{
destination: 'tokens.json',
format: 'json/flat',
},
],
},
// Tailwind config
tailwind: {
transformGroup: 'tokens-studio',
buildPath: 'config/',
files: [
{
destination: 'tailwind-tokens.js',
format: 'javascript/module-flat',
},
],
},
},
};
Add custom formats if needed:
// custom-formats.js
const StyleDictionary = require('style-dictionary');
// Format for FramingUI MCP server
StyleDictionary.registerFormat({
name: 'mcp/json',
formatter: function (dictionary) {
return JSON.stringify({
version: '1.0.0',
tokens: dictionary.allTokens.reduce((acc, token) => {
acc[token.path.join('.')] = {
value: token.value,
type: token.type,
description: token.description || '',
};
return acc;
}, {}),
}, null, 2);
},
});
module.exports = StyleDictionary;
Run transformation:
npx style-dictionary build
Output:
✓ Generated tokens.css
✓ Generated index.ts
✓ Generated tokens.json
✓ Generated tailwind-tokens.js
Stage 3: Automating with GitHub Actions
Set up continuous sync so every design change automatically flows to code.
Create workflow file:
# .github/workflows/sync-tokens.yml
name: Sync Design Tokens
on:
# Manual trigger
workflow_dispatch:
# Automatic trigger when design-tokens branch updates
push:
branches:
- design-tokens
paths:
- 'design-tokens/**'
# Scheduled polling (backup if plugin sync fails)
schedule:
- cron: '0 */6 * * *' # Every 6 hours
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Sync from Figma (if scheduled)
if: github.event_name == 'schedule'
env:
FIGMA_FILE_KEY: ${{ secrets.FIGMA_FILE_KEY }}
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
run: npm run sync:figma
- name: Transform tokens
run: npm run build:tokens
- name: Validate tokens
run: npm run validate:tokens
- name: Check for changes
id: changes
run: |
git diff --exit-code || echo "has_changes=true" >> $GITHUB_OUTPUT
- name: Commit changes
if: steps.changes.outputs.has_changes == 'true'
run: |
git config user.name "Token Sync Bot"
git config user.email "[email protected]"
git add .
git commit -m "chore: sync design tokens [skip ci]"
git push
- name: Create PR if on design-tokens branch
if: github.ref == 'refs/heads/design-tokens'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create \
--title "Design tokens update" \
--body "Automated sync from Figma Tokens Studio" \
--base main \
--head design-tokens \
|| echo "PR already exists"
Add validation script:
// scripts/validate-tokens.ts
import { readFileSync } from 'fs';
import { tokens } from '../src/tokens';
interface ValidationError {
path: string;
message: string;
}
function validate(): ValidationError[] {
const errors: ValidationError[] = [];
// Check for required token categories
const requiredCategories = ['color', 'spacing', 'typography', 'radius'];
for (const category of requiredCategories) {
if (!tokens[category]) {
errors.push({
path: category,
message: `Missing required token category: ${category}`,
});
}
}
// Check color token format
if (tokens.color) {
validateColorTokens(tokens.color, 'color', errors);
}
// Check spacing scale consistency
if (tokens.spacing) {
validateSpacingScale(tokens.spacing, 'spacing', errors);
}
return errors;
}
function validateColorTokens(obj: any, path: string, errors: ValidationError[]) {
for (const [key, value] of Object.entries(obj)) {
const currentPath = `${path}.${key}`;
if (typeof value === 'string') {
// Check hex color format
if (!value.match(/^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/)) {
errors.push({
path: currentPath,
message: `Invalid color format: ${value}`,
});
}
} else if (typeof value === 'object') {
validateColorTokens(value, currentPath, errors);
}
}
}
function validateSpacingScale(spacing: any, path: string, errors: ValidationError[]) {
const values = Object.values(spacing);
const numericValues = values
.map(v => parseFloat(v as string))
.filter(n => !isNaN(n))
.sort((a, b) => a - b);
// Check for consistent scale progression
for (let i = 1; i < numericValues.length; i++) {
const ratio = numericValues[i] / numericValues[i - 1];
if (ratio > 2) {
errors.push({
path,
message: `Spacing scale has inconsistent progression: ${numericValues[i - 1]} → ${numericValues[i]} (ratio: ${ratio.toFixed(2)})`,
});
}
}
}
const errors = validate();
if (errors.length > 0) {
console.error('Token validation failed:');
errors.forEach(err => {
console.error(` ${err.path}: ${err.message}`);
});
process.exit(1);
} else {
console.log('✓ All tokens valid');
}
Add to package.json:
{
"scripts": {
"sync:figma": "tsx scripts/sync-figma-tokens.ts",
"build:tokens": "style-dictionary build",
"validate:tokens": "tsx scripts/validate-tokens.ts",
"tokens": "npm run sync:figma && npm run build:tokens && npm run validate:tokens"
}
}
Stage 4: Enforcing Token Usage in Code Review
Automation prevents drift, but enforcement prevents regression. Use linters to block hardcoded values:
ESLint rule for no magic colors:
// eslint-rules/no-magic-colors.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow hardcoded color values',
category: 'Design System',
},
messages: {
noMagicColor: 'Use design tokens instead of hardcoded color "{{value}}"',
},
},
create(context) {
const colorRegex = /#[0-9A-Fa-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)/;
return {
Literal(node) {
if (typeof node.value === 'string' && colorRegex.test(node.value)) {
context.report({
node,
messageId: 'noMagicColor',
data: {
value: node.value,
},
});
}
},
TemplateElement(node) {
if (colorRegex.test(node.value.raw)) {
context.report({
node,
messageId: 'noMagicColor',
data: {
value: node.value.raw,
},
});
}
},
};
},
};
StyleLint rule for CSS:
// stylelint.config.js
module.exports = {
rules: {
'color-no-hex': true,
'declaration-property-value-disallowed-list': {
'/^color$/': ['/^#/', '/^rgb/', '/^hsl/'],
'/^background/': ['/^#/', '/^rgb/', '/^hsl/'],
'/^border/': ['/^#/', '/^rgb/', '/^hsl/'],
},
},
};
GitHub Actions check:
# .github/workflows/pr-checks.yml
name: PR Checks
on:
pull_request:
paths:
- 'src/**'
- 'components/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lint
- name: Check for hardcoded colors
run: |
if git diff origin/main...HEAD | grep -E '#[0-9A-Fa-f]{6}'; then
echo "Error: Hardcoded color values detected"
echo "Please use design tokens instead"
exit 1
fi
Stage 5: Runtime Token Updates (Advanced)
For dynamic theming or A/B testing, support runtime token updates:
// lib/token-provider.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface TokenContextValue {
tokens: Record<string, any>;
updateTokens: (newTokens: Record<string, any>) => void;
}
const TokenContext = createContext<TokenContextValue | null>(null);
export function TokenProvider({ children }: { children: ReactNode }) {
const [tokens, setTokens] = useState(() => {
// Load initial tokens
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('design-tokens');
if (stored) return JSON.parse(stored);
}
return require('../tokens').tokens;
});
useEffect(() => {
// Subscribe to token updates from admin panel
const ws = new WebSocket(process.env.NEXT_PUBLIC_TOKENS_WS_URL!);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === 'tokens-update') {
setTokens(update.tokens);
localStorage.setItem('design-tokens', JSON.stringify(update.tokens));
}
};
return () => ws.close();
}, []);
const updateTokens = (newTokens: Record<string, any>) => {
setTokens(newTokens);
localStorage.setItem('design-tokens', JSON.stringify(newTokens));
};
return (
<TokenContext.Provider value={{ tokens, updateTokens }}>
{children}
</TokenContext.Provider>
);
}
export function useTokens() {
const context = useContext(TokenContext);
if (!context) throw new Error('useTokens must be used within TokenProvider');
return context;
}
Usage in components:
import { useTokens } from '@/lib/token-provider';
export function Button({ variant = 'primary', children }: ButtonProps) {
const { tokens } = useTokens();
const styles = {
backgroundColor: tokens.color.action[variant].default,
color: tokens.color.text.onAction,
padding: `${tokens.spacing[2]} ${tokens.spacing[4]}`,
borderRadius: tokens.radius.md,
};
return <button style={styles}>{children}</button>;
}
Monitoring and Observability
Track token sync health:
// lib/token-metrics.ts
export function trackTokenSync(event: {
source: 'figma' | 'github' | 'manual';
tokensChanged: number;
duration: number;
success: boolean;
error?: string;
}) {
// Send to analytics
if (typeof window !== 'undefined' && window.plausible) {
window.plausible('Token Sync', {
props: {
source: event.source,
tokensChanged: event.tokensChanged,
duration: event.duration,
success: event.success,
error: event.error,
},
});
}
// Log for debugging
console.log('[Token Sync]', event);
}
Add to sync script:
async function main() {
const startTime = Date.now();
let tokensChanged = 0;
try {
const before = readFileSync('./design-tokens/tokens.json', 'utf-8');
await syncFigmaTokens();
const after = readFileSync('./design-tokens/tokens.json', 'utf-8');
tokensChanged = countChanges(before, after);
trackTokenSync({
source: 'figma',
tokensChanged,
duration: Date.now() - startTime,
success: true,
});
} catch (error) {
trackTokenSync({
source: 'figma',
tokensChanged: 0,
duration: Date.now() - startTime,
success: false,
error: error.message,
});
throw error;
}
}
Real-World Example: Complete Automation Flow
Here's how it works end-to-end:
1. Designer updates button color in Figma
- Changes
action/primaryfrom#2563ebto#3b82f6 - Saves (Tokens Studio plugin auto-commits to
design-tokensbranch)
2. GitHub Actions triggers on branch push
- Runs
style-dictionary build - Generates updated CSS, TypeScript, JSON files
- Validates token structure
- Creates PR to
main
3. PR checks run
- Lint validation passes (no hardcoded colors in diff)
- Token validation passes
- Preview deployment shows updated colors
4. Developer reviews and merges PR
- No code changes needed
- All components using token references automatically get new color
5. Production deployment
- New tokens deploy with next release
- Runtime token provider picks up changes (if using dynamic updates)
- Users see updated UI
Total time from design update to production: ~15 minutes (mostly CI/CD pipeline time). No manual intervention.
Integrating with FramingUI
If you're using FramingUI for AI-assisted development, the token automation feeds directly into AI context:
// mcp-server/design-tokens.ts (FramingUI MCP integration)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { readFileSync } from 'fs';
const server = new Server({
name: 'design-tokens',
version: '1.0.0',
});
server.setRequestHandler('resources/read', async (request) => {
if (request.params.uri === 'design://tokens') {
// Read latest transformed tokens
const tokens = JSON.parse(
readFileSync('./src/tokens/index.json', 'utf-8')
);
return {
contents: [{
uri: 'design://tokens',
mimeType: 'application/json',
text: JSON.stringify(tokens, null, 2),
}],
};
}
});
Now when Claude Code generates components via FramingUI, it always references the latest synced tokens—no stale context.
Conclusion
Token automation transforms design systems from aspirational documentation into enforced infrastructure. The workflow requires setup—CI/CD configuration, validation scripts, linting rules—but eliminates an entire class of consistency problems.
Designers update Figma. Tokens sync automatically. Code references semantic names. Production stays consistent. The system enforces what manual process couldn't.
The tools exist. The patterns are proven. The remaining question is whether your team prioritizes automation or continues manual synchronization until the inevitable drift crisis forces change.