Theming a component library is tedious. Every component needs color variants for light/dark modes, size variants for different use cases, and state variants for interactions. Multiply that by dozens of components, and you're maintaining hundreds of permutations manually.
AI changes the economics of theming. Instead of manually defining every variant, you define the rules—then let AI generate the permutations. Update a token value, re-run generation, and every variant updates consistently.
This guide walks through setting up AI-powered theming for a component library, from initial token structure to automated variant generation at scale.
The Theming Problem
Traditional theming requires manually defining every possible combination:
// Manual approach - unsustainable at scale
const Button = styled.button`
/* Default theme, default size, default state */
background-color: #2563eb;
color: white;
padding: 8px 16px;
/* Default theme, default size, hover state */
&:hover {
background-color: #1d4ed8;
}
/* Default theme, small size, default state */
&.size-sm {
padding: 4px 12px;
font-size: 14px;
}
/* Default theme, small size, hover state */
&.size-sm:hover {
background-color: #1d4ed8;
}
/* Dark theme, default size, default state */
.dark & {
background-color: #3b82f6;
}
/* Dark theme, default size, hover state */
.dark &:hover {
background-color: #2563eb;
}
/* ... and so on for every combination */
`;
This approach has fatal flaws:
- Exponential complexity - Each new variant multiplies permutations
- Inconsistency - Easy to miss combinations or use wrong values
- No reusability - Every component duplicates theme logic
- Maintenance nightmare - Updating one color requires changing dozens of lines
AI-powered theming inverts this: define the structure once, generate variants automatically.
Architecture: Token-Driven Theme Generation
The system has four layers:
[1. Core Tokens]
↓
[2. Semantic Mappings]
↓
[3. Component Recipes]
↓
[4. Generated Variants] ← AI generates this layer
Core tokens are raw values (colors, sizes, spacings).
Semantic mappings assign meaning to raw values (action colors, text colors, surface colors).
Component recipes define how semantic tokens combine for each component type.
Generated variants are the actual CSS/component code AI produces from recipes.
Let's build each layer.
Layer 1: Core Token Foundation
Start with a structured token set:
// tokens/core.ts
export const core = {
color: {
// Base palette
blue: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
red: {
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
},
green: {
50: '#f0fdf4',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
},
yellow: {
50: '#fefce8',
500: '#eab308',
600: '#ca8a04',
700: '#a16207',
},
},
spacing: {
0: '0',
1: '0.25rem', // 4px
2: '0.5rem', // 8px
3: '0.75rem', // 12px
4: '1rem', // 16px
5: '1.25rem', // 20px
6: '1.5rem', // 24px
8: '2rem', // 32px
10: '2.5rem', // 40px
12: '3rem', // 48px
},
fontSize: {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
},
fontWeight: {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
},
radius: {
none: '0',
sm: '0.25rem', // 4px
base: '0.375rem', // 6px
md: '0.5rem', // 8px
lg: '0.75rem', // 12px
full: '9999px',
},
shadow: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
base: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
},
} as const;
These are the raw ingredients. Next, map them semantically.
Layer 2: Semantic Theme Mappings
Create separate mappings for light and dark themes:
// tokens/semantic.ts
import { core } from './core';
export const light = {
color: {
action: {
primary: {
default: core.color.blue[600],
hover: core.color.blue[700],
active: core.color.blue[800],
disabled: core.color.gray[300],
},
secondary: {
default: core.color.gray[600],
hover: core.color.gray[700],
active: core.color.gray[800],
disabled: core.color.gray[200],
},
destructive: {
default: core.color.red[600],
hover: core.color.red[700],
active: core.color.red[800],
disabled: core.color.red[200],
},
},
text: {
primary: core.color.gray[900],
secondary: core.color.gray[600],
tertiary: core.color.gray[500],
disabled: core.color.gray[400],
onAction: '#ffffff',
link: core.color.blue[600],
linkHover: core.color.blue[700],
},
surface: {
default: '#ffffff',
subtle: core.color.gray[50],
elevated: '#ffffff',
overlay: 'rgba(0, 0, 0, 0.5)',
},
border: {
default: core.color.gray[200],
strong: core.color.gray[300],
subtle: core.color.gray[100],
focus: core.color.blue[500],
},
feedback: {
info: {
bg: core.color.blue[50],
text: core.color.blue[900],
border: core.color.blue[200],
},
success: {
bg: core.color.green[50],
text: core.color.green[900],
border: core.color.green[200],
},
warning: {
bg: core.color.yellow[50],
text: core.color.yellow[900],
border: core.color.yellow[200],
},
error: {
bg: core.color.red[50],
text: core.color.red[900],
border: core.color.red[200],
},
},
},
} as const;
export const dark = {
color: {
action: {
primary: {
default: core.color.blue[500],
hover: core.color.blue[400],
active: core.color.blue[600],
disabled: core.color.gray[700],
},
secondary: {
default: core.color.gray[500],
hover: core.color.gray[400],
active: core.color.gray[600],
disabled: core.color.gray[800],
},
destructive: {
default: core.color.red[500],
hover: core.color.red[400],
active: core.color.red[600],
disabled: core.color.gray[700],
},
},
text: {
primary: core.color.gray[50],
secondary: core.color.gray[400],
tertiary: core.color.gray[500],
disabled: core.color.gray[600],
onAction: '#ffffff',
link: core.color.blue[400],
linkHover: core.color.blue[300],
},
surface: {
default: core.color.gray[900],
subtle: core.color.gray[800],
elevated: core.color.gray[800],
overlay: 'rgba(0, 0, 0, 0.7)',
},
border: {
default: core.color.gray[700],
strong: core.color.gray[600],
subtle: core.color.gray[800],
focus: core.color.blue[500],
},
feedback: {
info: {
bg: core.color.blue[900],
text: core.color.blue[100],
border: core.color.blue[700],
},
success: {
bg: core.color.green[900],
text: core.color.green[100],
border: core.color.green[700],
},
warning: {
bg: core.color.yellow[900],
text: core.color.yellow[100],
border: core.color.yellow[700],
},
error: {
bg: core.color.red[900],
text: core.color.red[100],
border: core.color.red[700],
},
},
},
} as const;
// Export combined theme object
export const theme = { light, dark } as const;
Now you have structured mappings for both themes. AI can reference these to generate variants.
Layer 3: Component Recipes
Define how each component uses semantic tokens:
// recipes/button.ts
export const buttonRecipe = {
base: {
fontWeight: 'medium',
borderRadius: 'md',
transition: 'all 0.2s',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
},
variants: {
variant: {
primary: {
light: {
backgroundColor: 'action.primary.default',
color: 'text.onAction',
'&:hover': { backgroundColor: 'action.primary.hover' },
'&:active': { backgroundColor: 'action.primary.active' },
'&:disabled': { backgroundColor: 'action.primary.disabled' },
},
dark: {
backgroundColor: 'action.primary.default',
color: 'text.onAction',
'&:hover': { backgroundColor: 'action.primary.hover' },
'&:active': { backgroundColor: 'action.primary.active' },
'&:disabled': { backgroundColor: 'action.primary.disabled' },
},
},
secondary: {
light: {
backgroundColor: 'action.secondary.default',
color: 'text.onAction',
'&:hover': { backgroundColor: 'action.secondary.hover' },
'&:active': { backgroundColor: 'action.secondary.active' },
'&:disabled': { backgroundColor: 'action.secondary.disabled' },
},
dark: {
backgroundColor: 'action.secondary.default',
color: 'text.onAction',
'&:hover': { backgroundColor: 'action.secondary.hover' },
'&:active': { backgroundColor: 'action.secondary.active' },
'&:disabled': { backgroundColor: 'action.secondary.disabled' },
},
},
outline: {
light: {
backgroundColor: 'transparent',
color: 'action.primary.default',
border: '1px solid',
borderColor: 'border.default',
'&:hover': {
backgroundColor: 'surface.subtle',
borderColor: 'action.primary.default',
},
},
dark: {
backgroundColor: 'transparent',
color: 'action.primary.default',
border: '1px solid',
borderColor: 'border.default',
'&:hover': {
backgroundColor: 'surface.subtle',
borderColor: 'action.primary.default',
},
},
},
},
size: {
sm: {
paddingX: 3,
paddingY: 1.5,
fontSize: 'sm',
},
md: {
paddingX: 4,
paddingY: 2,
fontSize: 'base',
},
lg: {
paddingX: 6,
paddingY: 3,
fontSize: 'lg',
},
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
};
This recipe describes the button's structure without hardcoding values. AI will resolve the token references.
Layer 4: AI-Powered Variant Generation
Now use AI to generate actual component code from the recipe:
// scripts/generate-components.ts
import Anthropic from '@anthropic-ai/sdk';
import { writeFileSync } from 'fs';
import { buttonRecipe } from './recipes/button';
import { theme } from './tokens/semantic';
import { core } from './tokens/core';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!,
});
async function generateComponent(recipe: any, componentName: string) {
const prompt = `
Generate a React component implementing this design recipe:
Component name: ${componentName}
Recipe: ${JSON.stringify(recipe, null, 2)}
Theme tokens available:
${JSON.stringify({ theme, core }, null, 2)}
Requirements:
1. Use TypeScript with proper types
2. Support all variants defined in recipe
3. Use class-variance-authority (cva) for variant composition
4. Resolve token references to actual theme values
5. Include proper TypeScript types for props
6. Add JSDoc comments
7. Export both the component and its prop types
Generate complete, production-ready code.
`;
const response = await anthropic.messages.create({
model: 'claude-sonnet-4',
max_tokens: 4096,
messages: [{
role: 'user',
content: prompt,
}],
});
return response.content[0].text;
}
async function main() {
console.log('Generating Button component...');
const buttonCode = await generateComponent(buttonRecipe, 'Button');
writeFileSync('./src/components/Button.tsx', buttonCode);
console.log('✓ Button component generated');
}
main();
Generated output:
import { cva, type VariantProps } from 'class-variance-authority';
import { forwardRef } from 'react';
import { theme } from '@/tokens/semantic';
import { core } from '@/tokens/core';
/**
* Button component with support for multiple variants and sizes.
* Automatically adapts to light/dark theme context.
*/
const buttonVariants = cva(
[
'inline-flex items-center justify-center',
'font-medium transition-all duration-200 cursor-pointer',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
],
{
variants: {
variant: {
primary: [
'text-white',
'light:bg-[--color-action-primary-default] light:hover:bg-[--color-action-primary-hover] light:active:bg-[--color-action-primary-active]',
'dark:bg-[--color-action-primary-default] dark:hover:bg-[--color-action-primary-hover] dark:active:bg-[--color-action-primary-active]',
],
secondary: [
'text-white',
'light:bg-[--color-action-secondary-default] light:hover:bg-[--color-action-secondary-hover] light:active:bg-[--color-action-secondary-active]',
'dark:bg-[--color-action-secondary-default] dark:hover:bg-[--color-action-secondary-hover] dark:active:bg-[--color-action-secondary-active]',
],
outline: [
'bg-transparent border',
'light:text-[--color-action-primary-default] light:border-[--color-border-default] light:hover:bg-[--color-surface-subtle] light:hover:border-[--color-action-primary-default]',
'dark:text-[--color-action-primary-default] dark:border-[--color-border-default] dark:hover:bg-[--color-surface-subtle] dark:hover:border-[--color-action-primary-default]',
],
destructive: [
'text-white',
'light:bg-[--color-action-destructive-default] light:hover:bg-[--color-action-destructive-hover] light:active:bg-[--color-action-destructive-active]',
'dark:bg-[--color-action-destructive-default] dark:hover:bg-[--color-action-destructive-hover] dark:active:bg-[--color-action-destructive-active]',
],
},
size: {
sm: 'px-3 py-1.5 text-sm rounded-md',
md: 'px-4 py-2 text-base rounded-md',
lg: 'px-6 py-3 text-lg rounded-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/** Button content */
children: React.ReactNode;
/** Whether button spans full width */
fullWidth?: boolean;
}
/**
* Button component
*
* @example
* ```tsx
* <Button variant="primary" size="md">
* Click me
* </Button>
* ```
*/
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, fullWidth, ...props }, ref) => {
return (
<button
className={buttonVariants({
variant,
size,
className: fullWidth ? `w-full ${className}` : className,
})}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
AI generated:
- Complete TypeScript implementation
- All variant combinations from recipe
- Proper theme-aware styling
- Accessibility attributes
- JSDoc documentation
- Type-safe props
Scaling to Multiple Components
Create recipes for all components, then batch generate:
// scripts/generate-all.ts
import { buttonRecipe } from './recipes/button';
import { inputRecipe } from './recipes/input';
import { cardRecipe } from './recipes/card';
import { alertRecipe } from './recipes/alert';
// ... import all recipes
const components = [
{ recipe: buttonRecipe, name: 'Button' },
{ recipe: inputRecipe, name: 'Input' },
{ recipe: cardRecipe, name: 'Card' },
{ recipe: alertRecipe, name: 'Alert' },
// ... add all components
];
async function generateAll() {
for (const { recipe, name } of components) {
console.log(`Generating ${name}...`);
const code = await generateComponent(recipe, name);
writeFileSync(`./src/components/${name}.tsx`, code);
console.log(`✓ ${name} generated`);
}
console.log('\n✓ All components generated');
}
generateAll();
Run once, get a complete themed component library.
Automating Theme Updates
When design tokens change, regenerate all components automatically:
# .github/workflows/regenerate-components.yml
name: Regenerate Components
on:
push:
paths:
- 'tokens/**'
- 'recipes/**'
jobs:
regenerate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Generate components
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: npm run generate:components
- name: Format code
run: npm run format
- name: Commit changes
run: |
git config user.name "Component Generator"
git config user.email "[email protected]"
git add src/components/
git commit -m "chore: regenerate components from updated tokens/recipes"
git push
Now any token update triggers component regeneration. The entire library stays synchronized automatically.
Adding Custom Variants
AI can generate new variants on demand. Add a new variant to the recipe:
// recipes/button.ts - add new variant
variant: {
// ... existing variants
ghost: {
light: {
backgroundColor: 'transparent',
color: 'action.primary.default',
'&:hover': { backgroundColor: 'surface.subtle' },
},
dark: {
backgroundColor: 'transparent',
color: 'action.primary.default',
'&:hover': { backgroundColor: 'surface.subtle' },
},
},
}
Re-run generation:
npm run generate:components
AI updates the Button component to include the ghost variant. No manual code changes needed.
Theme Customization for Different Brands
Generate multiple theme variants from the same recipes:
// tokens/brand-a.ts
export const brandA = {
light: {
color: {
action: {
primary: {
default: '#0066cc', // Brand A blue
hover: '#0052a3',
active: '#003d7a',
},
},
// ... rest of theme
},
},
dark: { /* ... */ },
};
// tokens/brand-b.ts
export const brandB = {
light: {
color: {
action: {
primary: {
default: '#00a86b', // Brand B green
hover: '#008c59',
active: '#007047',
},
},
// ... rest of theme
},
},
dark: { /* ... */ },
};
Generate separate component libraries for each brand:
async function generateBrandThemes() {
const brands = [
{ theme: brandA, name: 'BrandA' },
{ theme: brandB, name: 'BrandB' },
];
for (const { theme, name } of brands) {
console.log(`\nGenerating ${name} theme...`);
for (const component of components) {
const code = await generateComponent(
component.recipe,
component.name,
theme // Pass specific theme
);
writeFileSync(
`./src/themes/${name}/${component.name}.tsx`,
code
);
}
console.log(`✓ ${name} theme complete`);
}
}
Output: complete component libraries for multiple brands, all maintained from shared recipes.
Integrating with FramingUI
If you're using FramingUI as your base design system, you can extend its theming with AI generation:
// Import FramingUI tokens
import { tokens as framingTokens } from 'framingui';
// Extend with your custom theme
export const myTheme = {
...framingTokens,
color: {
...framingTokens.color,
brand: {
primary: '#ff6b6b',
secondary: '#4ecdc4',
},
},
};
// Generate components using extended theme
const prompt = `
Generate themed components using this token set:
${JSON.stringify(myTheme, null, 2)}
Base on FramingUI patterns but customize with brand colors.
`;
AI generates components that follow FramingUI conventions but use your custom theme.
Advanced: Dynamic Theme Generation from Design Files
Poll Figma for theme updates and auto-generate:
// scripts/sync-figma-themes.ts
import { fetchFigmaFile, extractThemeTokens } from './figma-sync';
async function syncThemes() {
// Fetch latest from Figma
const figmaFile = await fetchFigmaFile(process.env.FIGMA_FILE_ID!);
// Extract theme tokens
const themes = extractThemeTokens(figmaFile);
// Generate component library for each theme
for (const [themeName, tokens] of Object.entries(themes)) {
console.log(`\nGenerating ${themeName} theme...`);
// Write tokens
writeFileSync(
`./tokens/${themeName}.ts`,
`export const ${themeName} = ${JSON.stringify(tokens, null, 2)} as const;`
);
// Generate components with this theme
await generateComponentsWithTheme(tokens, themeName);
}
}
// Run on schedule
setInterval(syncThemes, 1000 * 60 * 30); // Every 30 minutes
Designer updates Figma → Script syncs tokens → AI regenerates components → Production deploys new theme. Fully automated.
Performance Optimization
AI generation can be slow. Optimize with caching:
import crypto from 'crypto';
function getRecipeHash(recipe: any): string {
return crypto
.createHash('md5')
.update(JSON.stringify(recipe))
.digest('hex');
}
async function generateComponent(recipe: any, name: string) {
const hash = getRecipeHash(recipe);
const cachePath = `.cache/${name}-${hash}.tsx`;
// Check cache
if (existsSync(cachePath)) {
console.log(`Using cached ${name}`);
return readFileSync(cachePath, 'utf-8');
}
// Generate fresh
const code = await callAI(recipe, name);
// Save to cache
writeFileSync(cachePath, code);
return code;
}
Only regenerate components when recipes actually change.
Measuring Success
Track these metrics after implementing AI theming:
Time savings:
- Before: ~30 minutes per component variant
- After: ~2 minutes (generation + review)
- At 50 components × 4 variants = 1400 minutes saved (23+ hours)
Consistency:
- Manual theming: 15-20% variant inconsistencies
- AI theming: <2% (mostly caught in review)
Maintenance:
- Manual: ~4 hours per theme update
- AI: ~15 minutes (regenerate + deploy)
Conclusion
AI-powered theming transforms component libraries from high-maintenance liabilities into self-updating infrastructure. Define token structure and component recipes once, then let AI handle the exponential complexity of variant generation.
The approach scales to any size library. Whether you have 10 components or 100, the workflow remains constant: update tokens/recipes, regenerate, review, deploy.
The tooling exists—Claude API, GitHub Actions, Style Dictionary. The pattern works—token-driven generation with AI execution. The question is whether your team continues manually maintaining hundreds of theme variants or automates the entire process.