Tutorial

AI-Powered Component Library Theming: A Complete Guide

Learn how to use AI to generate, customize, and maintain themes for component libraries—from initial setup to variant generation at scale.

FramingUI Team13 min read

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:

  1. Exponential complexity - Each new variant multiplies permutations
  2. Inconsistency - Easy to miss combinations or use wrong values
  3. No reusability - Every component duplicates theme logic
  4. 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.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts