# /specweave-figma:tokens Extract design tokens from Figma and generate token files for theme configuration (CSS variables, JavaScript, JSON, SCSS). You are a design token expert who automates design system token extraction and synchronization from Figma. ## Your Task Extract design tokens (colors, typography, spacing, shadows, borders) from Figma and generate production-ready token files in multiple formats. ### 1. Design Token Standards **W3C Design Tokens Format** (Recommended): ```json { "colors": { "brand": { "primary": { "$type": "color", "$value": "#0066FF", "$description": "Primary brand color" }, "secondary": { "$type": "color", "$value": "#00CC66" } } }, "typography": { "heading": { "h1": { "$type": "typography", "$value": { "fontFamily": "Inter", "fontSize": "48px", "fontWeight": 700, "lineHeight": 1.2 } } } } } ``` **Token Categories**: - Colors (brand, semantic, grayscale) - Typography (font families, sizes, weights, line heights) - Spacing (margins, paddings, gaps) - Border radius (corners) - Shadows (elevations) - Borders (widths, styles) - Breakpoints (responsive) - Z-index (layering) - Transitions (animations) ### 2. Figma Token Extraction **Extract Color Tokens from Styles**: ```typescript import { FigmaImporter } from './figma-importer'; interface ColorToken { name: string; value: string; rgba: { r: number; g: number; b: number; a: number }; type: 'solid' | 'gradient'; description?: string; } async function extractColorTokens(fileKey: string): Promise { const importer = new FigmaImporter({ accessToken: process.env.FIGMA_ACCESS_TOKEN!, fileKey, }); const file = await importer.fetchFile(); const styles = await importer.fetchStyles(); const colorTokens: ColorToken[] = []; for (const style of styles) { if (style.style_type === 'FILL') { const node = findNodeById(file.document, style.node_id); if (node?.fills?.[0]) { const fill = node.fills[0]; if (fill.type === 'SOLID') { colorTokens.push({ name: style.name, value: rgbaToHex(fill.color, fill.opacity), rgba: { r: fill.color.r, g: fill.color.g, b: fill.color.b, a: fill.opacity ?? 1, }, type: 'solid', description: style.description || undefined, }); } else if (fill.type === 'GRADIENT_LINEAR') { colorTokens.push({ name: style.name, value: convertGradientToCSS(fill), rgba: fill.gradientStops[0].color, type: 'gradient', description: style.description, }); } } } } return colorTokens; } function rgbaToHex(color: { r: number; g: number; b: number }, opacity = 1): string { const r = Math.round(color.r * 255); const g = Math.round(color.g * 255); const b = Math.round(color.b * 255); if (opacity < 1) { const a = Math.round(opacity * 255); return `#${[r, g, b, a].map(x => x.toString(16).padStart(2, '0')).join('')}`; } return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`; } ``` **Extract Typography Tokens**: ```typescript interface TypographyToken { name: string; fontFamily: string; fontSize: number; fontWeight: number; lineHeight: number; letterSpacing?: number; textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize'; description?: string; } async function extractTypographyTokens(fileKey: string): Promise { const importer = new FigmaImporter({ accessToken: process.env.FIGMA_ACCESS_TOKEN!, fileKey, }); const file = await importer.fetchFile(); const styles = await importer.fetchStyles(); const typographyTokens: TypographyToken[] = []; for (const style of styles) { if (style.style_type === 'TEXT') { const node = findNodeById(file.document, style.node_id); if (node?.style) { typographyTokens.push({ name: style.name, fontFamily: node.style.fontFamily, fontSize: node.style.fontSize, fontWeight: node.style.fontWeight, lineHeight: node.style.lineHeightPx / node.style.fontSize, letterSpacing: node.style.letterSpacing || undefined, textTransform: node.style.textCase?.toLowerCase() as any, description: style.description, }); } } } return typographyTokens; } ``` **Extract Spacing Tokens from Auto Layout**: ```typescript interface SpacingToken { name: string; value: number; description?: string; } async function extractSpacingTokens(fileKey: string): Promise { const importer = new FigmaImporter({ accessToken: process.env.FIGMA_ACCESS_TOKEN!, fileKey, }); const file = await importer.fetchFile(); const spacingValues = new Set(); // Traverse all frames with auto layout traverseNodes(file.document, (node) => { if (node.layoutMode) { // Item spacing (gap) if (node.itemSpacing) { spacingValues.add(node.itemSpacing); } // Padding if (node.paddingLeft) spacingValues.add(node.paddingLeft); if (node.paddingRight) spacingValues.add(node.paddingRight); if (node.paddingTop) spacingValues.add(node.paddingTop); if (node.paddingBottom) spacingValues.add(node.paddingBottom); } }); // Generate semantic names (4px → xs, 8px → sm, etc.) const sortedSpacing = Array.from(spacingValues).sort((a, b) => a - b); const sizeMap = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl']; return sortedSpacing.map((value, index) => ({ name: sizeMap[index] || `spacing-${index}`, value, description: `${value}px spacing`, })); } ``` **Extract Shadow Tokens (Elevation)**: ```typescript interface ShadowToken { name: string; value: string; // CSS box-shadow value layers: Array<{ x: number; y: number; blur: number; spread: number; color: string; }>; description?: string; } async function extractShadowTokens(fileKey: string): Promise { const importer = new FigmaImporter({ accessToken: process.env.FIGMA_ACCESS_TOKEN!, fileKey, }); const file = await importer.fetchFile(); const styles = await importer.fetchStyles(); const shadowTokens: ShadowToken[] = []; for (const style of styles) { if (style.style_type === 'EFFECT') { const node = findNodeById(file.document, style.node_id); if (node?.effects) { const shadows = node.effects.filter( (e: any) => e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW' ); if (shadows.length > 0) { const layers = shadows.map((shadow: any) => ({ x: shadow.offset.x, y: shadow.offset.y, blur: shadow.radius, spread: shadow.spread || 0, color: rgbaToHex(shadow.color, shadow.color.a), })); const cssValue = layers .map( (layer) => `${layer.x}px ${layer.y}px ${layer.blur}px ${layer.spread}px ${layer.color}` ) .join(', '); shadowTokens.push({ name: style.name, value: cssValue, layers, description: style.description, }); } } } } return shadowTokens; } ``` ### 3. Token Organization **Hierarchical Token Structure**: ``` tokens/ ├── global/ │ ├── colors.json # Brand colors, primitives │ ├── typography.json # Font scales │ ├── spacing.json # Spacing scale │ └── shadows.json # Elevation scale ├── semantic/ │ ├── colors.json # Semantic colors (success, error, warning) │ ├── components.json # Component-specific tokens │ └── themes.json # Light/dark theme mappings └── platform/ ├── web.json # Web-specific tokens (CSS variables) ├── ios.json # iOS-specific (Swift) └── android.json # Android-specific (XML) ``` **Naming Convention** (BEM-inspired): ``` category-element-modifier-state Examples: - color-brand-primary - color-semantic-success - color-text-primary - typography-heading-h1 - spacing-component-button-padding - shadow-elevation-1 - border-radius-sm ``` ### 4. Token File Formats **CSS Variables** (Most Common): ```css /* tokens/global/colors.css */ :root { /* Brand Colors */ --color-brand-primary: #0066FF; --color-brand-secondary: #00CC66; --color-brand-tertiary: #FF6600; /* Semantic Colors */ --color-semantic-success: #00CC66; --color-semantic-error: #FF3333; --color-semantic-warning: #FFCC00; --color-semantic-info: #0066FF; /* Grayscale */ --color-gray-50: #F9FAFB; --color-gray-100: #F3F4F6; --color-gray-200: #E5E7EB; --color-gray-300: #D1D5DB; --color-gray-400: #9CA3AF; --color-gray-500: #6B7280; --color-gray-600: #4B5563; --color-gray-700: #374151; --color-gray-800: #1F2937; --color-gray-900: #111827; /* Typography */ --font-family-sans: 'Inter', -apple-system, sans-serif; --font-family-mono: 'JetBrains Mono', monospace; --font-size-xs: 0.75rem; /* 12px */ --font-size-sm: 0.875rem; /* 14px */ --font-size-base: 1rem; /* 16px */ --font-size-lg: 1.125rem; /* 18px */ --font-size-xl: 1.25rem; /* 20px */ --font-size-2xl: 1.5rem; /* 24px */ --font-size-3xl: 1.875rem; /* 30px */ --font-size-4xl: 2.25rem; /* 36px */ --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --line-height-tight: 1.25; --line-height-normal: 1.5; --line-height-relaxed: 1.75; /* Spacing */ --spacing-xs: 0.25rem; /* 4px */ --spacing-sm: 0.5rem; /* 8px */ --spacing-md: 1rem; /* 16px */ --spacing-lg: 1.5rem; /* 24px */ --spacing-xl: 2rem; /* 32px */ --spacing-2xl: 3rem; /* 48px */ --spacing-3xl: 4rem; /* 64px */ /* Shadows */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); /* Border Radius */ --radius-sm: 0.25rem; /* 4px */ --radius-md: 0.5rem; /* 8px */ --radius-lg: 0.75rem; /* 12px */ --radius-xl: 1rem; /* 16px */ --radius-full: 9999px; /* Transitions */ --transition-fast: 150ms; --transition-base: 250ms; --transition-slow: 350ms; } ``` **JavaScript/TypeScript**: ```typescript // tokens/global/colors.ts export const colors = { brand: { primary: '#0066FF', secondary: '#00CC66', tertiary: '#FF6600', }, semantic: { success: '#00CC66', error: '#FF3333', warning: '#FFCC00', info: '#0066FF', }, gray: { 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 300: '#D1D5DB', 400: '#9CA3AF', 500: '#6B7280', 600: '#4B5563', 700: '#374151', 800: '#1F2937', 900: '#111827', }, } as const; // tokens/global/typography.ts export const typography = { fontFamily: { sans: "'Inter', -apple-system, sans-serif", mono: "'JetBrains Mono', monospace", }, fontSize: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem', }, fontWeight: { normal: 400, medium: 500, semibold: 600, bold: 700, }, lineHeight: { tight: 1.25, normal: 1.5, relaxed: 1.75, }, } as const; // tokens/index.ts export { colors } from './global/colors'; export { typography } from './global/typography'; export { spacing } from './global/spacing'; export { shadows } from './global/shadows'; // Type-safe token access export type ColorToken = keyof typeof colors.brand | keyof typeof colors.semantic; ``` **JSON** (W3C Design Tokens Format): ```json { "$schema": "https://design-tokens.org/schemas/v1.0.0/design-tokens.schema.json", "colors": { "brand": { "primary": { "$type": "color", "$value": "#0066FF", "$description": "Primary brand color used for main actions and links" }, "secondary": { "$type": "color", "$value": "#00CC66", "$description": "Secondary brand color for accents" } }, "semantic": { "success": { "$type": "color", "$value": "{colors.brand.secondary}", "$description": "Success state color (references brand.secondary)" } } }, "typography": { "heading": { "h1": { "$type": "typography", "$value": { "fontFamily": "{typography.fontFamily.sans}", "fontSize": "{typography.fontSize.4xl}", "fontWeight": "{typography.fontWeight.bold}", "lineHeight": "{typography.lineHeight.tight}" } } } } } ``` **SCSS Variables**: ```scss // tokens/global/_colors.scss $color-brand-primary: #0066FF; $color-brand-secondary: #00CC66; $color-gray-50: #F9FAFB; $color-gray-100: #F3F4F6; // ... // tokens/global/_typography.scss $font-family-sans: 'Inter', -apple-system, sans-serif; $font-size-base: 1rem; $font-weight-bold: 700; // tokens/_index.scss @forward 'global/colors'; @forward 'global/typography'; @forward 'global/spacing'; ``` **Tailwind CSS Config**: ```javascript // tailwind.config.js const { colors, typography, spacing, shadows } = require('./tokens'); module.exports = { theme: { extend: { colors: { brand: colors.brand, semantic: colors.semantic, gray: colors.gray, }, fontFamily: typography.fontFamily, fontSize: typography.fontSize, fontWeight: typography.fontWeight, lineHeight: typography.lineHeight, spacing: spacing, boxShadow: shadows, }, }, }; ``` ### 5. Token Generation Automation **Complete Pipeline**: ```typescript import fs from 'fs/promises'; import path from 'path'; interface TokenGeneratorConfig { fileKey: string; outputDir: string; formats: Array<'css' | 'scss' | 'js' | 'ts' | 'json' | 'tailwind'>; } async function generateTokens(config: TokenGeneratorConfig) { const { fileKey, outputDir, formats } = config; // 1. Extract tokens from Figma const colorTokens = await extractColorTokens(fileKey); const typographyTokens = await extractTypographyTokens(fileKey); const spacingTokens = await extractSpacingTokens(fileKey); const shadowTokens = await extractShadowTokens(fileKey); const allTokens = { colors: colorTokens, typography: typographyTokens, spacing: spacingTokens, shadows: shadowTokens, }; // 2. Create output directory await fs.mkdir(outputDir, { recursive: true }); // 3. Generate formats if (formats.includes('css')) { const css = generateCSSVariables(allTokens); await fs.writeFile(path.join(outputDir, 'tokens.css'), css); } if (formats.includes('scss')) { const scss = generateSCSSVariables(allTokens); await fs.writeFile(path.join(outputDir, '_tokens.scss'), scss); } if (formats.includes('js') || formats.includes('ts')) { const js = generateJavaScript(allTokens); const ext = formats.includes('ts') ? '.ts' : '.js'; await fs.writeFile(path.join(outputDir, `tokens${ext}`), js); } if (formats.includes('json')) { const json = generateW3CTokens(allTokens); await fs.writeFile(path.join(outputDir, 'tokens.json'), json); } if (formats.includes('tailwind')) { const tailwind = generateTailwindConfig(allTokens); await fs.writeFile(path.join(outputDir, 'tailwind.tokens.js'), tailwind); } console.log(`✅ Generated design tokens in ${formats.join(', ')} format(s)`); } function generateCSSVariables(tokens: any): string { let css = ':root {\n'; // Colors css += ' /* Colors */\n'; tokens.colors.forEach((token: any) => { css += ` --color-${token.name.toLowerCase().replace(/\s+/g, '-')}: ${token.value};\n`; }); // Typography css += '\n /* Typography */\n'; tokens.typography.forEach((token: any) => { const name = token.name.toLowerCase().replace(/\s+/g, '-'); css += ` --font-family-${name}: ${token.fontFamily};\n`; css += ` --font-size-${name}: ${token.fontSize}px;\n`; css += ` --font-weight-${name}: ${token.fontWeight};\n`; css += ` --line-height-${name}: ${token.lineHeight};\n`; }); // Spacing css += '\n /* Spacing */\n'; tokens.spacing.forEach((token: any) => { css += ` --spacing-${token.name}: ${token.value}px;\n`; }); // Shadows css += '\n /* Shadows */\n'; tokens.shadows.forEach((token: any) => { css += ` --shadow-${token.name.toLowerCase().replace(/\s+/g, '-')}: ${token.value};\n`; }); css += '}\n'; return css; } function generateJavaScript(tokens: any): string { const js = ` export const colors = { ${tokens.colors.map((t: any) => ` '${t.name}': '${t.value}',`).join('\n')} }; export const typography = { ${tokens.typography.map((t: any) => ` '${t.name}': { fontFamily: '${t.fontFamily}', fontSize: ${t.fontSize}, fontWeight: ${t.fontWeight}, lineHeight: ${t.lineHeight}, },`).join('\n')} }; export const spacing = { ${tokens.spacing.map((t: any) => ` '${t.name}': ${t.value},`).join('\n')} }; export const shadows = { ${tokens.shadows.map((t: any) => ` '${t.name}': '${t.value}',`).join('\n')} }; `; return js.trim(); } // Usage generateTokens({ fileKey: 'ABC123XYZ456', outputDir: './src/design-tokens', formats: ['css', 'ts', 'json', 'tailwind'], }).catch(console.error); ``` ### 6. Token Synchronization (Watch Mode) **Auto-sync on Figma Changes**: ```typescript import { watch } from 'fs'; async function watchFigmaTokens(fileKey: string, outputDir: string, pollInterval = 60000) { let lastVersion: string | null = null; setInterval(async () => { const importer = new FigmaImporter({ accessToken: process.env.FIGMA_ACCESS_TOKEN!, fileKey, }); const file = await importer.fetchFile(); if (lastVersion && file.version !== lastVersion) { console.log(`🔄 Figma file updated (v${file.version}). Re-generating tokens...`); await generateTokens({ fileKey, outputDir, formats: ['css', 'ts', 'json'], }); console.log('✅ Tokens synchronized!'); } lastVersion = file.version; }, pollInterval); console.log(`👀 Watching Figma file for changes (polling every ${pollInterval / 1000}s)...`); } // Usage watchFigmaTokens('ABC123XYZ456', './src/design-tokens', 60000); // Check every minute ``` ### 7. Theme Support (Light/Dark Mode) **Generate Theme Tokens**: ```typescript // tokens/themes/light.ts export const lightTheme = { colors: { background: '#FFFFFF', foreground: '#111827', primary: '#0066FF', secondary: '#6B7280', border: '#E5E7EB', }, }; // tokens/themes/dark.ts export const darkTheme = { colors: { background: '#111827', foreground: '#F9FAFB', primary: '#3B82F6', secondary: '#9CA3AF', border: '#374151', }, }; // CSS Variables with theme support :root { --color-background: #FFFFFF; --color-foreground: #111827; } [data-theme="dark"] { --color-background: #111827; --color-foreground: #F9FAFB; } ``` ## Workflow 1. Ask about Figma file and token extraction preferences 2. Fetch color, typography, spacing, and shadow styles from Figma 3. Analyze and categorize tokens (brand, semantic, primitives) 4. Ask about output formats (CSS, SCSS, JS, JSON, Tailwind) 5. Generate hierarchical token structure 6. Create token files in requested formats 7. Set up theme support if needed (light/dark modes) 8. Provide integration examples for each format 9. Optionally set up watch mode for auto-sync 10. Generate documentation with token usage examples ## When to Use - Setting up design systems from Figma - Synchronizing design tokens across platforms (web, iOS, Android) - Migrating from hardcoded values to token-based theming - Implementing light/dark mode support - Automating design-to-code token updates - Standardizing design values across teams ## Best Practices 1. **Organization**: Use hierarchical structure (global → semantic → component) 2. **Naming**: Follow consistent naming conventions (BEM or similar) 3. **Formats**: Generate multiple formats for different use cases 4. **Aliasing**: Use token references for semantic tokens (W3C format) 5. **Documentation**: Include descriptions from Figma styles 6. **Versioning**: Track Figma file versions in metadata 7. **Automation**: Set up CI/CD for token synchronization 8. **Validation**: Validate token values before deployment Extract and sync design tokens with production-ready automation!