Верификация дизайн-системы
Updated Jul 2026
Проблема: дрифт между Figma и кодом
Разрыв между тем, что дизайнеры создают в Figma, и тем, что разработчики реализуют в коде, является постоянным источником визуальных багов. Дизайнер указывает padding: 16px. Разработчик оценивает «примерно 16» и использует padding: 1rem (что равно 16px при стандартном размере шрифта, но масштабируется иначе). За месяцы эти мелкие различия накапливаются в заметный дрифт дизайна.
Автоматическая верификация обнаруживает этот дрифт до его накопления.
Что верифицировать
| Аспект | Источник в Figma | Цель в коде | Метод верификации |
|---|---|---|---|
| Цвета | Дизайн-токены | CSS-переменные / тема | Извлечение и сравнение hex-значений |
| Типографика | Семейства шрифтов, размеры, начертания | Вычисленные CSS-стили | Автоматическое извлечение стилей |
| Отступы | Значения padding, margin | Вычисленные CSS-стили | Измерение в пикселях |
| Скругление | Значения радиуса углов | CSS border-radius |
Сравнение вычисленных стилей |
| Тени | Параметры drop shadow | CSS box-shadow |
Сравнение значений |
| Иконки | SVG-ассеты | SVG в коде | Сравнение хэшей файлов |
| Состояния компонентов | Hover, active, disabled, focus | Интерактивные состояния | Визуальное тестирование по состояниям |
Автоматическая верификация дизайн-токенов
// 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
# 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
Рабочий процесс дизайн-токенов
Пайплайн генерации токенов
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
Пример 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
# .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
});
}
}
Верификация дизайн-системы предотвращает постепенное расхождение между дизайнерским замыслом и реализацией, которое ручное ревью неизбежно пропускает. Автоматическое сравнение спецификаций Figma и вычисленных CSS-стилей обнаруживает дрифт на уровне PR, до того как он накопится в десятках компонентов.