Design System Verification
The Problem: Figma-to-Code Drift
The gap between what designers create in Figma and what developers implement in code is a persistent source of visual bugs. A designer specifies padding: 16px. A developer estimates "about 16" and uses padding: 1rem (which is 16px at default font size but scales differently). Over months, these small differences compound into noticeable design drift.
Automated verification catches this drift before it compounds.
What to Verify
| Aspect | Figma Source | Code Target | Verification Method |
|---|---|---|---|
| Colors | Design tokens | CSS variables / theme | Extract and compare hex values |
| Typography | Font families, sizes, weights | CSS computed styles | Automated style extraction |
| Spacing | Padding, margin values | CSS computed styles | Pixel measurement |
| Border radius | Corner radius values | CSS border-radius |
Computed style comparison |
| Shadows | Drop shadow parameters | CSS box-shadow |
Value comparison |
| Icons | SVG assets | SVG in code | File hash comparison |
| Component states | Hover, active, disabled, focus | Interactive states | Visual testing per state |
Automated Design Token Verification
// tests/design-system/token-verification.spec.ts
import { test, expect } from '@playwright/test';
import designTokens from '../../design-tokens/tokens.json';
test.describe('Design Token Verification', () => {
test('color tokens match implementation', async ({ page }) => {
await page.goto('/design-system/colors');
for (const [name, expectedValue] of Object.entries(designTokens.colors)) {
// Query the CSS custom property value
const actualValue = await page.evaluate((varName) => {
return getComputedStyle(document.documentElement)
.getPropertyValue(`--color-${varName}`)
.trim();
}, name);
expect(actualValue).toBe(expectedValue);
}
});
test('typography tokens match implementation', async ({ page }) => {
await page.goto('/design-system/typography');
const typographySpecs = designTokens.typography;
for (const [variant, spec] of Object.entries(typographySpecs)) {
const element = page.locator(`[data-typography="${variant}"]`);
const computedStyles = await element.evaluate((el) => {
const styles = window.getComputedStyle(el);
return {
fontFamily: styles.fontFamily,
fontSize: styles.fontSize,
fontWeight: styles.fontWeight,
lineHeight: styles.lineHeight,
letterSpacing: styles.letterSpacing,
};
});
expect(computedStyles.fontSize).toBe((spec as any).fontSize);
expect(computedStyles.fontWeight).toBe(String((spec as any).fontWeight));
expect(computedStyles.lineHeight).toBe((spec as any).lineHeight);
}
});
test('spacing scale matches design tokens', async ({ page }) => {
await page.goto('/design-system/spacing');
const spacingScale = designTokens.spacing;
for (const [size, expectedPx] of Object.entries(spacingScale)) {
const actualValue = await page.evaluate((varName) => {
return getComputedStyle(document.documentElement)
.getPropertyValue(`--spacing-${varName}`)
.trim();
}, size);
expect(actualValue).toBe(expectedPx);
}
});
});
Figma API Integration
# scripts/figma_design_drift.py
"""
Compare Figma component specifications with live implementation.
Detects drift between design and code.
"""
import requests
import json
FIGMA_TOKEN = "your-figma-api-token"
FIGMA_FILE_ID = "your-file-id"
def get_figma_component_styles(component_name: str):
"""Extract style properties from a Figma component."""
url = f"https://api.figma.com/v1/files/{FIGMA_FILE_ID}/components"
headers = {"X-Figma-Token": FIGMA_TOKEN}
response = requests.get(url, headers=headers)
components = response.json()
for component in components.get("meta", {}).get("components", []):
if component["name"] == component_name:
# Get component node details
node_url = f"https://api.figma.com/v1/files/{FIGMA_FILE_ID}/nodes?ids={component['node_id']}"
node_response = requests.get(node_url, headers=headers)
node_data = node_response.json()
# Extract visual properties
node = list(node_data["nodes"].values())[0]["document"]
return {
"fills": node.get("fills", []),
"strokes": node.get("strokes", []),
"cornerRadius": node.get("cornerRadius"),
"padding": {
"top": node.get("paddingTop"),
"right": node.get("paddingRight"),
"bottom": node.get("paddingBottom"),
"left": node.get("paddingLeft"),
},
"width": node.get("absoluteBoundingBox", {}).get("width"),
"height": node.get("absoluteBoundingBox", {}).get("height"),
}
return None
def compare_with_implementation(figma_styles: dict, computed_styles: dict):
"""Compare Figma specs with browser computed styles."""
drift_report = []
if figma_styles.get("cornerRadius"):
figma_radius = f"{figma_styles['cornerRadius']}px"
if computed_styles.get("borderRadius") != figma_radius:
drift_report.append({
"property": "border-radius",
"figma": figma_radius,
"implementation": computed_styles.get("borderRadius"),
})
# Compare padding
for side in ["top", "right", "bottom", "left"]:
figma_padding = figma_styles.get("padding", {}).get(side)
if figma_padding is not None:
figma_px = f"{figma_padding}px"
impl_key = f"padding{side.capitalize()}"
if computed_styles.get(impl_key) != figma_px:
drift_report.append({
"property": f"padding-{side}",
"figma": figma_px,
"implementation": computed_styles.get(impl_key),
})
return drift_report
Design Token Workflow
Token Generation Pipeline
Figma (design source)
|
+-- Style Dictionary / Figma Tokens plugin
| exports tokens.json
|
+-- Token transformer (Style Dictionary)
| generates:
| +-- css/variables.css (CSS custom properties)
| +-- ts/tokens.ts (TypeScript constants)
| +-- ios/tokens.swift (iOS)
| +-- android/tokens.xml (Android)
|
+-- CI verification
compares:
+-- tokens.json vs computed CSS properties
+-- Figma API values vs tokens.json
Example tokens.json
{
"colors": {
"primary-500": "#3B82F6",
"primary-600": "#2563EB",
"primary-700": "#1D4ED8",
"neutral-100": "#F3F4F6",
"neutral-900": "#111827",
"error-500": "#EF4444",
"success-500": "#22C55E"
},
"typography": {
"heading-1": {
"fontSize": "36px",
"fontWeight": 700,
"lineHeight": "44px"
},
"heading-2": {
"fontSize": "24px",
"fontWeight": 600,
"lineHeight": "32px"
},
"body": {
"fontSize": "16px",
"fontWeight": 400,
"lineHeight": "24px"
}
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px",
"2xl": "48px"
},
"radii": {
"sm": "4px",
"md": "8px",
"lg": "12px",
"full": "9999px"
}
}
CI Integration
# .github/workflows/design-system.yml
name: Design System Verification
on:
pull_request:
paths:
- 'src/components/**'
- 'design-tokens/**'
- 'src/styles/**'
jobs:
verify-tokens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build && npm run start &
- name: Verify design tokens
run: npx playwright test tests/design-system/
- name: Check Figma drift
run: python scripts/figma_design_drift.py --report drift-report.json
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
- name: Comment drift report on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (fs.existsSync('drift-report.json')) {
const report = JSON.parse(fs.readFileSync('drift-report.json'));
if (report.length > 0) {
const body = `## Design Drift Detected\n\n` +
report.map(d =>
`- **${d.property}**: Figma=${d.figma}, Code=${d.implementation}`
).join('\n');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
}
}
Design system verification prevents the gradual divergence between design intent and implementation that manual review inevitably misses. Automated comparison between Figma specs and computed CSS styles catches drift at the PR level, before it compounds across dozens of components.