design-tokens
Turn product ideas into production launches with Spec-Driven Development. Repeatable Claude Code workflows with quality gates, token budgets, and auditable artifacts.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- GitHub stars: 12
- Updated: 2025-11-21
Download the latest version (v1.0.0) from this page.
SKILL.md
# Design Tokens - Comprehensive Reference
**Version**: 2.0.0
**Purpose**: Generate and maintain design system tokens across all project surfaces (UI, emails, PDFs, CLI, charts, docs)
---
## Overview
This skill provides comprehensive guidance for initializing and maintaining design tokens in `design/systems/tokens.json` and `tokens.css`. Supports both brownfield (scan existing code) and greenfield (generate new palette) modes.
**Coverage**:
- UI components (React/Vue/Svelte)
- MJML/HTML emails
- PDF styling (WeasyPrint, Puppeteer)
- CLI colors (chalk/colorette)
- Data visualization (d3, Chart.js, Recharts)
- Documentation assets (MD/MDX)
---
## What Changed in v2
**Non-UI surface coverage**:
- MJML/HTML emails (inline CSS)
- PDF styling (WeasyPrint, Puppeteer CSS)
- CLI colors (chalk/colorette)
- Data visualization palettes (d3, Chart.js, Recharts)
- Documentation assets (MD/MDX)
**Brownfield scan improvements**:
- Source-agnostic: JSX/TSX, CSS/SCSS, CSS-in-JS, MD/MDX, MJML, Python/JS PDF styles
- Scans 10+ file types
- Detects arbitrary Tailwind values (`bg-[#xxx]`)
**Contrast enforcement**:
- Auto-nudges OKLCH lightness until WCAG 2.1 passes (≥4.5:1)
- Uses colorjs.io for perceptually accurate contrast calculation
**Tailwind v4 alignment**:
- Theme variables as CSS variables (not hardcoded palette)
- Validates tokens.css import in globals.css
**Color-blind-safe palettes**:
- Okabe-Ito categorical palette for charts
- 8 colors scientifically validated for color-blind users
---
## Mental Model
**Flow**: `detect mode → scan/generate → consolidate → validate wiring → contrast fixups → emit artifacts → report debt`
**Brownfield**: Scan existing code for patterns across UI, emails, PDFs, CLI, charts, docs. Consolidate duplicates and propose migrations.
**Greenfield**: Ask minimal questions, generate OKLCH palette with contrast validation.
---
## Prerequisites
**Dependencies**:
```json
{
"devDependencies": {
"colorjs.io": "^0.5.0",
"picocolors": "^1.0.0",
"fs-extra": "^11.2.0"
}
}
```
**Install**:
```bash
pnpm add -D colorjs.io picocolors fs-extra
```
---
## Brownfield Scan (Multi-Surface)
**Scanned locations**:
| Surface | Paths | File Types | Patterns Detected |
|---------|-------|------------|-------------------|
| **UI** | `app/**`, `src/**`, `components/**` | `.tsx`, `.jsx`, `.css`, `.scss` | Hex colors, Tailwind classes, CSS-in-JS |
| **Emails** | `emails/**`, `templates/email/**` | `.mjml`, `.html` | Inline CSS, hex colors |
| **PDFs** | `server/pdf/**`, `reports/**` | `.css`, `.py` (WeasyPrint) | PDF-specific CSS, hex in templates |
| **CLI** | `scripts/**`, `bin/**` | `.js`, `.mjs`, `.ts` | chalk/colorette color names |
| **Charts** | `components/**`, `lib/**` | `.tsx`, `.js` | d3/Chart.js/Recharts palettes |
| **Docs** | `docs/**`, `*.md`, `*.mdx` | `.md`, `.mdx` | Fenced CSS blocks |
**Scan output**:
```
design/systems/
├── detected-tokens.json # All detected colors with usage counts
├── token-analysis-report.md # Consolidation opportunities
├── violations-colors.txt # Hex colors found (file:line:color)
└── violations-arbitrary.txt # Tailwind arbitrary values (file:line:value)
```
**Example violations-colors.txt**:
```
#3b82f6 47 (most used - primary candidate)
#3b81f6 3 (duplicate - consolidate)
#10b981 28 (success candidate)
#ef4444 15 (error candidate)
```
---
## Token Consolidation
**Consolidation rules**:
- Colors → 12 semantic + 7 neutral shades
- Type scale → 8 sizes (xs, sm, base, lg, xl, 2xl, 3xl, 4xl)
- Spacing → 8px grid (~13 values: 0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96px)
**Semantic mapping**:
```javascript
{
brand: { primary, secondary, accent },
semantic: { success, error, warning, info },
neutral: { 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 }
}
```
**Minimal user questions** (greenfield only):
1. **Primary brand color**: Blue (#3b82f6), Purple (#8b5cf6), Green (#10b981), Custom
2. **Visual style**: Minimal (Stripe, Linear), Bold (Figma, Notion), Technical (Vercel, GitHub)
3. **Primary font**: Inter (geometric sans), Geist (humanist sans), System fonts
**Consolidation output**:
```
Before: 47 colors → After: 12 tokens (74% reduction)
Before: 14 font sizes → After: 8 sizes (43% reduction)
Before: 23 spacing values → After: 13 values (43% reduction)
Duplicates removed:
- #3b82f7, #3b81f6 → primary (#3b82f6)
- 15px, 17px → base (16px), lg (18px)
```
---
## Token Structure
**Complete token structure** (`design/systems/tokens.json`):
```json
{
"meta": {
"colorSpace": "oklch",
"fallbackSpace": "srgb",
"version": "2.0.0",
"generated": "2025-11-12T10:30:00Z"
},
"colors": {
"brand": {
"primary": {
"oklch": "oklch(59.69% 0.156 261.45)",
"fallback": "#3b82f6",
"description": "Primary brand color"
},
"secondary": {
"oklch": "oklch(58.23% 0.167 271.45)",
"fallback": "#6366f1"
},
"accent": {
"oklch": "oklch(67.89% 0.152 164.57)",
"fallback": "#10b981"
}
},
"semantic": {
"success": {
"bg": {"oklch": "oklch(95% 0.02 145)", "fallback": "#D1FAE5"},
"fg": {"oklch": "oklch(25% 0.12 145)", "fallback": "#047857"},
"border": {"oklch": "oklch(85% 0.05 145)", "fallback": "#A7F3D0"},
"icon": {"oklch": "oklch(35% 0.13 145)", "fallback": "#059669"}
},
"error": {
"bg": {"oklch": "oklch(95% 0.02 27)", "fallback": "#FEE2E2"},
"fg": {"oklch": "oklch(30% 0.18 27)", "fallback": "#991B1B"},
"border": {"oklch": "oklch(85% 0.08 27)", "fallback": "#FECACA"},
"icon": {"oklch": "oklch(40% 0.20 27)", "fallback": "#DC2626"}
},
"warning": {
"bg": {"oklch": "oklch(95% 0.02 90)", "fallback": "#FEF3C7"},
"fg": {"oklch": "oklch(35% 0.15 90)", "fallback": "#92400E"},
"border": {"oklch": "oklch(85% 0.08 90)", "fallback": "#FDE68A"},
"icon": {"oklch": "oklch(45% 0.16 90)", "fallback": "#D97706"}
},
"info": {
"bg": {"oklch": "oklch(95% 0.02 240)", "fallback": "#DBEAFE"},
"fg": {"oklch": "oklch(30% 0.12 240)", "fallback": "#1E40AF"},
"border": {"oklch": "oklch(85% 0.05 240)", "fallback": "#BFDBFE"},
"icon": {"oklch": "oklch(40% 0.14 240)", "fallback": "#3B82F6"}
}
},
"neutral": {
"50": {"oklch": "oklch(98% 0 0)", "fallback": "#fafafa"},
"100": {"oklch": "oklch(96% 0 0)", "fallback": "#f5f5f5"},
"200": {"oklch": "oklch(90% 0 0)", "fallback": "#e5e5e5"},
"300": {"oklch": "oklch(82% 0 0)", "fallback": "#d4d4d4"},
"400": {"oklch": "oklch(64% 0 0)", "fallback": "#a3a3a3"},
"500": {"oklch": "oklch(54% 0 0)", "fallback": "#737373"},
"600": {"oklch": "oklch(42% 0 0)", "fallback": "#525252"},
"700": {"oklch": "oklch(32% 0 0)", "fallback": "#404040"},
"800": {"oklch": "oklch(23% 0 0)", "fallback": "#262626"},
"900": {"oklch": "oklch(15% 0 0)", "fallback": "#171717"},
"950": {"oklch": "oklch(11% 0 0)", "fallback": "#0a0a0a"}
}
},
"typography": {
"families": {
"sans": "Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
"mono": "Fira Code, Menlo, Monaco, Consolas, monospace",
"serif": "Georgia, Cambria, 'Times New Roman', Times, serif"
},
"sizes": {
"xs": "0.75rem",
"sm": "0.875rem",
"base": "1rem",
"lg": "1.125rem",
"xl": "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem"
},
"weights": {
"normal": "400",
"medium": "500",
"semibold": "600",
"bold": "700"
},
"lineHeights": {
"tight": "1.25",
"normal": "1.5",
"relaxed": "1.75"
},
"letterSpacing": {
"display": "-0.025em",
"body": "0em",
"cta": "0.025em"
}
},
"spacing": {
"0": "0px",
"1": "4px",
"2": "8px",
"3": "12px",
"4": "16px",
"5": "20px",
"6": "24px",
"8": "32px",
"10": "40px",
"12": "48px",
"16": "64px",
"20": "80px",
"24": "96px"
},
"shadows": {
"light": {
"sm": {"value": "0 1px 2px oklch(0% 0 0 / 0.05)", "fallback": "0 1px 2px rgba(0, 0, 0, 0.05)"},
"md": {"value": "0 4px 6px oklch(0% 0 0 / 0.07), 0 2px 4px oklch(0% 0 0 / 0.06)", "fallback": "0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06)"},
"lg": {"value": "0 10px 15px oklch(0% 0 0 / 0.10), 0 4px 6px oklch(0% 0 0 / 0.05)", "fallback": "0 10px 15px rgba(0, 0, 0, 0.10), 0 4px 6px rgba(0, 0, 0, 0.05)"}
},
"dark": {
"sm": {"value": "0 1px 2px oklch(0% 0 0 / 0.30)", "fallback": "0 1px 2px rgba(0, 0, 0, 0.30)"},
"md": {"value": "0 4px 6px oklch(0% 0 0 / 0.35), 0 2px 4px oklch(0% 0 0 / 0.30)", "fallback": "0 4px 6px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.30)"},
"lg": {"value": "0 10px 15px oklch(0% 0 0 / 0.40), 0 4px 6px oklch(0% 0 0 / 0.35)", "fallback": "0 10px 15px rgba(0, 0, 0, 0.40), 0 4px 6px rgba(0, 0, 0, 0.35)"}
}
},
"motion": {
"duration": {
"fast": "150ms",
"base": "200ms",
"slow": "300ms",
"slower": "500ms"
},
"easing": {
"standard": "cubic-bezier(0.4, 0.0, 0.2, 1)",
"decelerate": "cubic-bezier(0.0, 0.0, 0.2, 1)",
"accelerate": "cubic-bezier(0.4, 0.0, 1.0, 1.0)"
}
},
"dataViz": {
"categorical": {
"okabe-ito": {
"orange": {"oklch": "oklch(68.29% 0.151 58.43)", "fallback": "#E69F00", "description": "Orange - warm accent"},
"skyBlue": {"oklch": "oklch(70.17% 0.099 232.66)", "fallback": "#56B4E9", "description": "Sky blue - cool primary"},
"bluishGreen": {"oklch": "oklch(59.78% 0.108 164.04)", "fallback": "#009E73", "description": "Bluish green - success"},
"yellow": {"oklch": "oklch(89.87% 0.162 99.57)", "fallback": "#F0E442", "description": "Yellow - warning"},
"blue": {"oklch": "oklch(46.86% 0.131 264.05)", "fallback": "#0072B2", "description": "Blue - information"},
"vermillion": {"oklch": "oklch(57.50% 0.199 37.70)", "fallback": "#D55E00", "description": "Vermillion - error"},
"reddishPurple": {"oklch": "oklch(50.27% 0.159 328.36)", "fallback": "#CC79A7", "description": "Reddish purple - accent"},
"black": {"oklch": "oklch(0% 0 0)", "fallback": "#000000", "description": "Black - text"}
}
},
"sequential": {
"blue": ["oklch(95% 0.02 240)", "oklch(75% 0.08 240)", "oklch(55% 0.12 240)", "oklch(35% 0.14 240)", "oklch(25% 0.16 240)"],
"green": ["oklch(95% 0.02 145)", "oklch(75% 0.08 145)", "oklch(55% 0.12 145)", "oklch(35% 0.14 145)", "oklch(25% 0.16 145)"]
},
"diverging": {
"red-blue": ["oklch(95% 0.02 27)", "oklch(75% 0.08 27)", "oklch(90% 0 0)", "oklch(75% 0.08 240)", "oklch(95% 0.02 240)"]
}
}
}
```
---
## WCAG Contrast Validation + Auto-Fix
**Validation pairs**:
- `semantic.success.fg` on `semantic.success.bg`
- `semantic.error.fg` on `semantic.error.bg`
- `semantic.warning.fg` on `semantic.warning.bg`
- `semantic.info.fg` on `semantic.info.bg`
- `brand.primary` on `neutral.50` (light background)
- `brand.primary` on `neutral.950` (dark background)
**Auto-fix algorithm**:
```javascript
import Color from "colorjs.io";
function autofixContrast(fgColor, bgColor, targetRatio = 4.5) {
const fg = new Color(fgColor);
const bg = new Color(bgColor);
let ratio = fg.contrast(bg, "WCAG21");
if (ratio >= targetRatio) return fg.toString({format: "oklch"});
// Nudge lightness until pass
let [L, C, H] = fg.oklch;
let iterations = 0;
const maxIterations = 60;
while (ratio < targetRatio && iterations < maxIterations) {
// Move L away from bg lightness
L = L < bg.oklch[0]
? Math.max(0, L - 0.01) // Darker
: Math.min(1, L + 0.01); // Lighter
const candidate = new Color("oklch", [L, C, H]);
ratio = candidate.contrast(bg, "WCAG21");
if (ratio >= targetRatio) {
return candidate.toString({format: "oklch"});
}
iterations++;
}
return fg.toString({format: "oklch"}); // Fallback if can't fix
}
```
**Validation output**:
```
✅ success.fg on success.bg: 10.23:1 (AAA)
✅ error.fg on error.bg: 9.87:1 (AAA)
✅ warning.fg on warning.bg: 8.45:1 (AAA)
✅ info.fg on info.bg: 10.12:1 (AAA)
✅ primary on neutral-50: 4.83:1 (AA for large text)
⚠️ primary on neutral-200: Auto-adjusted L from 59.69% → 52.31% (4.51:1)
```
---
## Tailwind v4 Wiring
**Checks**:
1. Tailwind config exists (`tailwind.config.js` or `tailwind.config.ts`)
2. `globals.css` imports `design/systems/tokens.css`
3. Colors use CSS variables (`var(--color-*)`) not hardcoded palette
4. Font families match tokens
5. Shadow scale matches elevation scale
**Expected globals.css**:
```css
@import '../design/systems/tokens.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
```
**Expected tailwind.config.js** (v4 style):
```javascript
/** @type {import('tailwindcss').Config} */
export default {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
secondary: 'var(--color-secondary)',
// ... map all tokens
}
}
}
}
```
**Validation errors**:
```
❌ Tailwind config not found
Fix: npx tailwindcss init
❌ Missing import in globals.css
Fix: Add @import '../design/systems/tokens.css';
⚠️ Hardcoded colors in tailwind.config.js
Found: colors: { primary: '#3b82f6' }
Expected: colors: { primary: 'var(--color-primary)' }
```
---
## CSS Variables Output
**Output** (`design/systems/tokens.css`):
```css
:root {
/* Brand colors (OKLCH with sRGB fallback) */
--color-primary: oklch(59.69% 0.156 261.45);
--color-primary-fallback: #3b82f6;
--color-secondary: oklch(58.23% 0.167 271.45);
--color-secondary-fallback: #6366f1;
--color-accent: oklch(67.89% 0.152 164.57);
--color-accent-fallback: #10b981;
/* Semantic colors (bg/fg/border/icon structure) */
--color-success-bg: oklch(95% 0.02 145);
--color-success-fg: oklch(25% 0.12 145);
--color-success-border: oklch(85% 0.05 145);
--color-success-icon: oklch(35% 0.13 145);
--color-error-bg: oklch(95% 0.02 27);
--color-error-fg: oklch(30% 0.18 27);
--color-error-border: oklch(85% 0.08 27);
--color-error-icon: oklch(40% 0.20 27);
--color-warning-bg: oklch(95% 0.02 90);
--color-warning-fg: oklch(35% 0.15 90);
--color-warning-border: oklch(85% 0.08 90);
--color-warning-icon: oklch(45% 0.16 90);
--color-info-bg: oklch(95% 0.02 240);
--color-info-fg: oklch(30% 0.12 240);
--color-info-border: oklch(85% 0.05 240);
--color-info-icon: oklch(40% 0.14 240);
/* Neutral palette */
--color-neutral-50: oklch(98% 0 0);
--color-neutral-100: oklch(96% 0 0);
--color-neutral-200: oklch(90% 0 0);
--color-neutral-300: oklch(82% 0 0);
--color-neutral-400: oklch(64% 0 0);
--color-neutral-500: oklch(54% 0 0);
--color-neutral-600: oklch(42% 0 0);
--color-neutral-700: oklch(32% 0 0);
--color-neutral-800: oklch(23% 0 0);
--color-neutral-900: oklch(15% 0 0);
--color-neutral-950: oklch(11% 0 0);
/* Typography - Families */
--font-sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: Fira Code, Menlo, Monaco, Consolas, monospace;
--font-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
/* Typography - Sizes */
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
/* Typography - Weights */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* Typography - Line Heights */
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Typography - Letter Spacing */
--letter-spacing-display: -0.025em;
--letter-spacing-body: 0em;
--letter-spacing-cta: 0.025em;
/* Spacing scale (4px grid) */
--spacing-0: 0px;
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-3: 12px;
--spacing-4: 16px;
--spacing-5: 20px;
--spacing-6: 24px;
--spacing-8: 32px;
--spacing-10: 40px;
--spacing-12: 48px;
--spacing-16: 64px;
--spacing-20: 80px;
--spacing-24: 96px;
/* Shadows - Light mode */
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.05);
--shadow-md: 0 4px 6px oklch(0% 0 0 / 0.07), 0 2px 4px oklch(0% 0 0 / 0.06);
--shadow-lg: 0 10px 15px oklch(0% 0 0 / 0.10), 0 4px 6px oklch(0% 0 0 / 0.05);
/* Motion - Duration */
--motion-duration-fast: 150ms;
--motion-duration-base: 200ms;
--motion-duration-slow: 300ms;
--motion-duration-slower: 500ms;
/* Motion - Easing */
--motion-easing-standard: cubic-bezier(0.4, 0.0, 0.2, 1);
--motion-easing-decelerate: cubic-bezier(0.0, 0.0, 0.2, 1);
--motion-easing-accelerate: cubic-bezier(0.4, 0.0, 1.0, 1.0);
/* Data visualization - Okabe-Ito (color-blind-safe) */
--dataviz-okabe-ito-orange: oklch(68.29% 0.151 58.43);
--dataviz-okabe-ito-sky-blue: oklch(70.17% 0.099 232.66);
--dataviz-okabe-ito-bluish-green: oklch(59.78% 0.108 164.04);
--dataviz-okabe-ito-yellow: oklch(89.87% 0.162 99.57);
--dataviz-okabe-ito-blue: oklch(46.86% 0.131 264.05);
--dataviz-okabe-ito-vermillion: oklch(57.50% 0.199 37.70);
--dataviz-okabe-ito-reddish-purple: oklch(50.27% 0.159 328.36);
--dataviz-okabe-ito-black: oklch(0% 0 0);
/* Data visualization - Sequential scales */
--dataviz-sequential-blue-1: oklch(95% 0.02 240);
--dataviz-sequential-blue-2: oklch(75% 0.08 240);
--dataviz-sequential-blue-3: oklch(55% 0.12 240);
--dataviz-sequential-blue-4: oklch(35% 0.14 240);
--dataviz-sequential-blue-5: oklch(25% 0.16 240);
--dataviz-sequential-green-1: oklch(95% 0.02 145);
--dataviz-sequential-green-2: oklch(75% 0.08 145);
--dataviz-sequential-green-3: oklch(55% 0.12 145);
--dataviz-sequential-green-4: oklch(35% 0.14 145);
--dataviz-sequential-green-5: oklch(25% 0.16 145);
/* Data visualization - Diverging scales */
--dataviz-diverging-red-blue-1: oklch(95% 0.02 27);
--dataviz-diverging-red-blue-2: oklch(75% 0.08 27);
--dataviz-diverging-red-blue-3: oklch(90% 0 0);
--dataviz-diverging-red-blue-4: oklch(75% 0.08 240);
--dataviz-diverging-red-blue-5: oklch(95% 0.02 240);
}
/* Dark mode shadows (4-6x opacity increase) */
@media (prefers-color-scheme: dark) {
:root {
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.30);
--shadow-md: 0 4px 6px oklch(0% 0 0 / 0.35), 0 2px 4px oklch(0% 0 0 / 0.30);
--shadow-lg: 0 10px 15px oklch(0% 0 0 / 0.40), 0 4px 6px oklch(0% 0 0 / 0.35);
}
}
/* Reduced motion (accessibility) */
@media (prefers-reduced-motion: reduce) {
:root {
--motion-duration-fast: 0ms;
--motion-duration-base: 0ms;
--motion-duration-slow: 0ms;
--motion-duration-slower: 0ms;
--motion-easing-standard: linear;
--motion-easing-decelerate: linear;
--motion-easing-accelerate: linear;
}
}
/* OKLCH fallback for legacy browsers (~8%) */
@supports not (color: oklch(0% 0 0)) {
:root {
--color-primary: var(--color-primary-fallback);
--color-secondary: var(--color-secondary-fallback);
--color-accent: var(--color-accent-fallback);
}
}
```
---
## Style Debt Reporting
**Detection**:
```bash
# Find hardcoded colors
grep -rnE "#[0-9a-fA-F]{6}" app src components emails server/pdf \
--include="*.tsx" --include="*.jsx" --include="*.css" --include="*.mjml" \
> design/systems/violations-colors.txt
# Find arbitrary Tailwind values
grep -rnE "bg-\[|text-\[|border-\[|p-\[|m-\[|shadow-\[" app src components \
--include="*.tsx" --include="*.jsx" \
> design/systems/violations-arbitrary.txt
```
**Report output**:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ Style Debt Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total violations: 127
Hardcoded hex colors: 89
Arbitrary Tailwind values: 38
Top offenders (hex