Using AI to Build Design System Components from Figma Specs
The Figma-to-Code Translation Problem
The Figma-to-Code Translation Problem
Every frontend developer knows this dance: the designer hands off a Figma file, you inspect the layers, try to match the spacing, eyeball the colors, and end up with something that is "close enough." Then the designer reviews it, points out 15 discrepancies, and you spend another day tweaking pixels.
This translation problem gets worse at scale. A design system with 40 components, each with 5 variants, means 200 specifications to implement perfectly. Manual translation is slow, error-prone, and mind-numbing.
I now use AI to handle the mechanical translation from Figma specs to React components. The designer still designs. I still make architectural decisions. But the tedious pixel-matching work is handled by AI, and the results are more accurate than my manual implementation ever was.
Step 1: Extract Design Tokens from Figma (10 Minutes)
Before building any components, extract the design tokens that define your visual language. In Figma, go to Local Styles and document:
Here are the design tokens from our Figma design system:
Colors:
- Primary: #2563EB (blue-600)
- Primary hover: #1D4ED8 (blue-700)
- Primary light: #DBEAFE (blue-100)
- Secondary: #475569 (slate-600)
- Background: #FFFFFF
- Surface: #F8FAFC (slate-50)
- Border: #E2E8F0 (slate-200)
- Text primary: #0F172A (slate-900)
- Text secondary: #64748B (slate-500)
- Error: #DC2626 (red-600)
- Success: #16A34A (green-600)
- Warning: #D97706 (amber-600)
Typography:
- Heading 1: Inter 32px/40px semibold
- Heading 2: Inter 24px/32px semibold
- Heading 3: Inter 20px/28px semibold
- Body: Inter 16px/24px regular
- Body small: Inter 14px/20px regular
- Caption: Inter 12px/16px regular
Spacing:
- Base unit: 4px
- Common spacings: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64
Border radius:
- Small: 4px (inputs, badges)
- Medium: 8px (cards, buttons)
- Large: 12px (modals, popovers)
- Full: 9999px (pills, avatars)
Shadows:
- Small: 0 1px 2px rgba(0,0,0,0.05)
- Medium: 0 4px 6px rgba(0,0,0,0.1)
- Large: 0 10px 15px rgba(0,0,0,0.1)
Generate a Tailwind CSS configuration that maps these tokens to
our design system. Output tailwind.config.ts with the extended theme.
Also create a CSS variables file for any values that Tailwind
cannot express natively.
The result is a single source of truth for your entire design system:
// tailwind.config.ts
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
brand: {
primary: '#2563EB',
'primary-hover': '#1D4ED8',
'primary-light': '#DBEAFE',
},
surface: {
DEFAULT: '#F8FAFC',
border: '#E2E8F0',
},
},
fontSize: {
'heading-1': ['2rem', { lineHeight: '2.5rem', fontWeight: '600' }],
'heading-2': ['1.5rem', { lineHeight: '2rem', fontWeight: '600' }],
'heading-3': ['1.25rem', { lineHeight: '1.75rem', fontWeight: '600' }],
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
boxShadow: {
sm: '0 1px 2px rgba(0,0,0,0.05)',
md: '0 4px 6px rgba(0,0,0,0.1)',
lg: '0 10px 15px rgba(0,0,0,0.1)',
},
},
},
} satisfies Config;
Step 2: Build One Component from Figma Specs (15 Minutes)
Take a single component from Figma and describe it precisely. I use this format:
Build a Button component based on this Figma specification:
Visual specs:
- Variants: primary, secondary, ghost, destructive
- Sizes: sm (32px height, 12px padding, 14px text), md (40px height, 16px padding, 16px text), lg (48px height, 24px padding, 16px text)
- Border radius: md (8px)
- Primary: bg brand-primary, text white, hover bg brand-primary-hover
- Secondary: bg white, border 1px slate-200, text slate-700, hover bg slate-50
- Ghost: bg transparent, text slate-600, hover bg slate-100
- Destructive: bg red-600, text white, hover bg red-700
- Disabled: opacity 50%, cursor not-allowed
- Focus: ring 2px brand-primary offset 2px
- Loading state: spinner icon replaces text, same dimensions
Behavior specs:
- Supports icon on left, right, or both
- Can render as <a> when href is provided (polymorphic)
- Loading state disables click and shows spinner
- Full-width variant stretches to container
Technical requirements:
- Use shadcn/ui Button as base, customize with our tokens
- Use class-variance-authority (cva) for variant management
- Use Slot pattern from @radix-ui for asChild composition
- TypeScript: strict props with discriminated union for link vs button
- Forwarded ref
- All ARIA attributes for loading and disabled states
File: src/components/ui/Button.tsx
This level of specificity produces components that match Figma specs on the first attempt. Here is what the output looks like:
// src/components/ui/Button.tsx
import { forwardRef } from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-brand-primary text-white hover:bg-brand-primary-hover',
secondary: 'border border-surface-border bg-white text-slate-700 hover:bg-slate-50',
ghost: 'text-slate-600 hover:bg-slate-100',
destructive: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-base',
lg: 'h-12 px-6 text-base',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
isLoading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, fullWidth, asChild, isLoading, leftIcon, rightIcon, children, disabled, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn(buttonVariants({ variant, size, fullWidth, className }))}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
) : leftIcon ? (
<span className="mr-2" aria-hidden="true">{leftIcon}</span>
) : null}
{children}
{rightIcon && !isLoading && (
<span className="ml-2" aria-hidden="true">{rightIcon}</span>
)}
</Comp>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
Step 3: Batch Generate Remaining Components (60 Minutes)
Once you have the pattern established with one component, batch the rest. For each component, provide the same level of Figma specification:
Build the following components using the same patterns as our Button component
(cva variants, forwardRef, strict TypeScript, shadcn/ui base):
1. Input — text input with label, helper text, error state, icon slots
2. Select — dropdown select with search, multi-select option, groups
3. Card — container with header, body, footer sections
4. Badge — status indicator with dot variant, color variants
5. Avatar — image with fallback initials, sizes, status indicator
6. Tooltip — hover tooltip with configurable placement and delay
For each component, I will provide the Figma specs separately.
Start with Input.
Then for each component:
Input component Figma specs:
Layout:
- Label: 14px text-secondary, 4px above input
- Input: 40px height, 12px horizontal padding, 16px text
- Helper text: 12px text-secondary, 4px below input
- Error text: 12px text-red-600, 4px below input (replaces helper when in error)
- Left icon: 20px, 12px from left edge, text-slate-400
- Right icon: 20px, 12px from right edge (clear button or custom)
States:
- Default: border 1px slate-200, bg white
- Hover: border slate-300
- Focus: border brand-primary, ring 2px brand-primary-light
- Error: border red-500, ring 2px red-100
- Disabled: bg slate-50, text slate-400
Variants: default, textarea (multi-line)
Sizes: sm (32px), md (40px), lg (48px)
Build src/components/ui/Input.tsx with full TypeScript types and forwarded ref.
Include aria-describedby linking to helper/error text.
Step 4: Generate Component Stories and Visual Tests (20 Minutes)
For each component, generate Storybook stories that serve as both documentation and visual regression tests:
For the Button and Input components we just built, create Storybook stories:
File: src/components/ui/Button.stories.tsx
Stories to include:
1. Default — each variant side by side
2. Sizes — each size for primary variant
3. WithIcons — left icon, right icon, both
4. Loading — loading state for each variant
5. FullWidth — full-width variant
6. AsLink — button rendered as anchor tag
7. Disabled — disabled state for each variant
Use Storybook CSF3 format with TypeScript.
Each story should have args that match our component props.
Include a play function that tests keyboard interaction (Tab, Enter, Space).
Step 5: Validate Against Figma (10 Minutes)
The final step is overlaying your implementation against the Figma design. Take a screenshot of your component and the Figma design:
Compare the rendered Button component (screenshot attached) with the
Figma design spec. Check:
1. Height matches for each size (sm: 32px, md: 40px, lg: 48px)
2. Horizontal padding matches (sm: 12px, md: 16px, lg: 24px)
3. Font size and weight match
4. Border radius is exactly 8px
5. Color values match (compare hex values)
6. Spacing between icon and text is consistent
7. Focus ring matches the spec (2px, brand-primary, 2px offset)
List any discrepancies.
The Workflow in Practice
For a full design system of 25 components, here is the time breakdown:
| Step | Manual | AI-Assisted | |------|--------|-------------| | Token extraction | 2 hours | 10 minutes | | Build first component | 4 hours | 30 minutes | | Build remaining 24 | 5 days | 1.5 days | | Stories and docs | 3 days | 4 hours | | Visual QA and fixes | 2 days | 1 day | | Total | ~12 days | ~3 days |
The AI handles the mechanical work — translating specs to code, generating variants, creating stories. You handle the decisions — architecture, composition patterns, edge case behavior, and final quality review.
The result is a design system that matches Figma specs more accurately and is built 4x faster. Your designers are happy because the implementation is pixel-perfect. Your developers are happy because every component is typed, documented, and Storybooked.