AI code assistants can generate React components in seconds, but the styling is often generic—gray buttons, inconsistent spacing, arbitrary color values. The issue isn't AI capability; it's that most codebases don't provide AI-readable styling context. AI assistants need structured design tokens, variant definitions, and composition patterns to generate styled components that match your design system.
This guide walks through practical workflows for React component styling when AI handles the implementation, from token integration to complex variant systems.
The AI Styling Problem
When you prompt an AI to "create a user card component," it generates plausible but generic code:
// AI-generated without styling context
function UserCard({ name, email, avatar }: UserCardProps) {
return (
<div className="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<img
src={avatar}
alt={name}
className="w-16 h-16 rounded-full mb-4"
/>
<h3 className="text-xl font-bold text-gray-900">{name}</h3>
<p className="text-gray-600 text-sm">{email}</p>
<button className="mt-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
View Profile
</button>
</div>
);
}
Issues:
- Arbitrary colors:
bg-white,text-gray-900,bg-blue-500aren't from your design system - Manual spacing:
p-6,mb-4,mt-4don't match your spacing scale - No variant system: Can't easily create dark mode or different card styles
- Inline button: Doesn't use your Button component
- Hard-coded sizes:
w-16 h-16won't adapt to different card contexts
With proper styling context, the same prompt produces:
// AI-generated with design system context
function UserCard({ name, email, avatar }: UserCardProps) {
return (
<Card>
<CardContent className="flex items-center gap-4">
<Avatar src={avatar} alt={name} size="large" />
<div className="flex-1 space-y-1">
<CardTitle>{name}</CardTitle>
<Text variant="secondary" size="sm">{email}</Text>
</div>
<Button variant="secondary" size="small">
View Profile
</Button>
</CardContent>
</Card>
);
}
This version uses design system components, semantic tokens, and established spacing patterns. The difference is workflow, not AI capability.
Setting Up Design Tokens for AI Consumption
Design tokens must be structured for AI pattern matching. Three key requirements:
- Semantic naming that communicates intent
- Type definitions that AI can autocomplete
- Usage documentation that explains when to apply tokens
Start with color tokens:
// tokens/colors.ts
/**
* Semantic color tokens
*
* Naming convention: {category}-{role}-{variant}
* - Category: text, background (bg), border, etc.
* - Role: primary, secondary, tertiary (hierarchy)
* - Variant: default, hover, active, disabled (state)
*/
export const colors = {
/** Text colors with semantic hierarchy */
text: {
/** Primary text - use for headings and body copy */
primary: 'var(--color-text-primary)',
/** Secondary text - use for supporting information */
secondary: 'var(--color-text-secondary)',
/** Tertiary text - use for captions and metadata */
tertiary: 'var(--color-text-tertiary)',
/** Inverse text - use on dark backgrounds */
inverse: 'var(--color-text-inverse)',
/** Link text in default state */
link: 'var(--color-text-link)',
/** Link text in hover state */
linkHover: 'var(--color-text-link-hover)'
},
/** Background colors for surfaces */
background: {
/** Base page background */
base: 'var(--color-bg-base)',
/** Raised surface (cards, panels) */
raised: 'var(--color-bg-raised)',
/** Overlay surface (modals, popovers) */
overlay: 'var(--color-bg-overlay)',
/** Inverse background (tooltips on light background) */
inverse: 'var(--color-bg-inverse)'
},
/** Action colors for interactive elements */
action: {
primary: {
default: 'var(--color-action-primary-default)',
hover: 'var(--color-action-primary-hover)',
active: 'var(--color-action-primary-active)',
disabled: 'var(--color-action-primary-disabled)'
},
secondary: {
default: 'var(--color-action-secondary-default)',
hover: 'var(--color-action-secondary-hover)',
active: 'var(--color-action-secondary-active)',
disabled: 'var(--color-action-secondary-disabled)'
},
destructive: {
default: 'var(--color-action-destructive-default)',
hover: 'var(--color-action-destructive-hover)',
active: 'var(--color-action-destructive-active)',
disabled: 'var(--color-action-destructive-disabled)'
}
},
/** Border colors */
border: {
/** Default border color */
default: 'var(--color-border-default)',
/** Strong emphasis border */
strong: 'var(--color-border-strong)',
/** Subtle divider */
subtle: 'var(--color-border-subtle)',
/** Focus ring */
focus: 'var(--color-border-focus)'
}
} as const;
export type ColorToken = typeof colors;
The JSDoc comments train AI to understand usage. When it generates a button, it searches for "action" or "primary" and finds action.primary.default.
Spacing tokens:
// tokens/spacing.ts
/**
* Spacing scale based on 4px base unit
*
* Usage:
* - 0.5-2: Component internal spacing (padding, gaps)
* - 3-6: Between related elements (form fields, list items)
* - 8-12: Between sections
* - 16-24: Page-level margins
*/
export const spacing = {
/** 2px - Minimal spacing */
0.5: '0.125rem',
/** 4px - Tight spacing */
1: '0.25rem',
/** 8px - Compact spacing */
2: '0.5rem',
/** 12px - Comfortable spacing */
3: '0.75rem',
/** 16px - Standard spacing (most common) */
4: '1rem',
/** 20px */
5: '1.25rem',
/** 24px - Generous spacing */
6: '1.5rem',
/** 32px - Large spacing */
8: '2rem',
/** 40px */
10: '2.5rem',
/** 48px - Section spacing */
12: '3rem',
/** 64px - Large section spacing */
16: '4rem',
/** 96px - Page-level spacing */
24: '6rem'
} as const;
Configure Tailwind to use these tokens:
// tailwind.config.ts
import { colors, spacing } from './tokens';
function flattenTokens(obj: Record<string, any>, prefix = ''): Record<string, string> {
return Object.entries(obj).reduce((acc, [key, value]) => {
const newKey = prefix ? `${prefix}-${key}` : key;
if (typeof value === 'string') {
acc[newKey] = value;
} else if (typeof value === 'object') {
Object.assign(acc, flattenTokens(value, newKey));
}
return acc;
}, {} as Record<string, string>);
}
export default {
theme: {
extend: {
colors: flattenTokens(colors),
spacing: spacing
}
}
};
Now AI assistants can generate text-primary, bg-raised, p-4 that match your design system.
Building Variant-Based Components
AI assistants work best with prop-driven variants rather than class composition. Define component variants explicitly:
// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
/**
* Button variants using CVA
*
* AI Usage: Always use variant and size props instead of custom className
*/
const buttonVariants = cva(
// Base styles
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus disabled:pointer-events-none disabled:opacity-50',
{
variants: {
/**
* Visual style variant
* - primary: Main CTAs, one per screen
* - secondary: Alternative actions
* - outline: Tertiary actions
* - destructive: Delete/remove actions
* - ghost: Minimal emphasis
*/
variant: {
primary: 'bg-action-primary-default text-white hover:bg-action-primary-hover active:bg-action-primary-active',
secondary: 'bg-action-secondary-default text-white hover:bg-action-secondary-hover active:bg-action-secondary-active',
outline: 'border border-default bg-transparent hover:bg-raised',
destructive: 'bg-action-destructive-default text-white hover:bg-action-destructive-hover active:bg-action-destructive-active',
ghost: 'hover:bg-raised active:bg-raised'
},
/**
* Size variant
* - small: Compact UI, tables, toolbars
* - medium: Default size
* - large: Prominent CTAs
*/
size: {
small: 'h-8 px-3 text-sm',
medium: 'h-10 px-4 text-base',
large: 'h-12 px-6 text-lg'
}
},
defaultVariants: {
variant: 'primary',
size: 'medium'
}
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
/**
* Loading state - shows spinner and disables interaction
*/
isLoading?: boolean;
}
/**
* Button component
*
* @example
* ```tsx
* // Primary button (main CTA)
* <Button variant="primary">Sign Up</Button>
*
* // Secondary button (alternative action)
* <Button variant="secondary">Cancel</Button>
*
* // Destructive button (delete action)
* <Button variant="destructive">Delete Account</Button>
* ```
*/
export function Button({
variant,
size,
isLoading,
children,
disabled,
className,
...props
}: ButtonProps) {
return (
<button
className={buttonVariants({ variant, size, className })}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<>
<Spinner className="mr-2" size="sm" />
{children}
</>
) : (
children
)}
</button>
);
}
AI assistants read the JSDoc examples and generate correct usage:
// AI understands to use variant prop, not custom classes
<Button variant="primary">Create Account</Button>
<Button variant="destructive">Delete</Button>
Instead of:
// AI without variant context invents custom classes
<Button className="bg-blue-500 hover:bg-blue-600">Create Account</Button>
Composition Patterns for Complex Components
Teach AI how components compose using semantic wrappers:
// components/ui/card.tsx
/**
* Card - Container for grouped content
*
* Use composition: Card > CardHeader > CardTitle
* Don't apply padding manually inside Card
*/
export function Card({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'rounded-lg border border-default bg-raised shadow-sm',
className
)}
{...props}
>
{children}
</div>
);
}
/**
* CardHeader - Top section of card
* Contains CardTitle and optional CardDescription
*/
export function CardHeader({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('flex flex-col space-y-1.5 p-6 border-b border-subtle', className)}
{...props}
>
{children}
</div>
);
}
/**
* CardTitle - Main heading in card header
*/
export function CardTitle({
className,
children,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3
className={cn('text-lg font-semibold text-primary', className)}
{...props}
>
{children}
</h3>
);
}
/**
* CardDescription - Supporting text in card header
*/
export function CardDescription({
className,
children,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn('text-sm text-secondary', className)}
{...props}
>
{children}
</p>
);
}
/**
* CardContent - Main content area
*/
export function CardContent({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn('p-6', className)} {...props}>
{children}
</div>
);
}
/**
* CardFooter - Bottom section for actions
*/
export function CardFooter({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('flex items-center p-6 pt-0', className)}
{...props}
>
{children}
</div>
);
}
Document the composition pattern:
/**
* Card Component Usage
*
* @example
* ```tsx
* // ✅ Correct composition
* <Card>
* <CardHeader>
* <CardTitle>User Settings</CardTitle>
* <CardDescription>Manage your account preferences</CardDescription>
* </CardHeader>
* <CardContent>
* <form className="space-y-4">
* <Input label="Display Name" />
* <Input label="Email" type="email" />
* </form>
* </CardContent>
* <CardFooter>
* <Button variant="primary">Save Changes</Button>
* </CardFooter>
* </Card>
*
* // ❌ Incorrect - manual structure
* <Card>
* <div className="p-6 border-b">
* <h3 className="font-bold">User Settings</h3>
* </div>
* {/* Don't recreate header manually */}
* </Card>
* ```
*/
AI assistants see the correct example and generate proper composition.
Responsive Styling Patterns
AI assistants struggle with responsive design without explicit patterns. Define breakpoint conventions:
// tokens/breakpoints.ts
/**
* Responsive breakpoints
*
* Usage in Tailwind:
* - Mobile-first: default styles are mobile, add md:, lg: for larger screens
* - sm: 640px (small tablets)
* - md: 768px (tablets)
* - lg: 1024px (laptops)
* - xl: 1280px (desktops)
* - 2xl: 1536px (large desktops)
*
* @example
* ```tsx
* // Mobile: column, Desktop: row
* <div className="flex flex-col md:flex-row gap-4">
*
* // Mobile: full width, Desktop: constrained
* <div className="w-full md:w-1/2 lg:w-1/3">
*
* // Responsive padding
* <div className="px-4 md:px-6 lg:px-8">
* ```
*/
export const breakpoints = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px'
} as const;
Create responsive layout components:
// components/ui/container.tsx
/**
* Container - Responsive max-width wrapper
*
* Automatically constrains content width and centers
*/
export function Container({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8',
className
)}
{...props}
>
{children}
</div>
);
}
/**
* Grid - Responsive grid layout
*
* @example
* ```tsx
* // 1 column mobile, 2 tablet, 3 desktop
* <Grid cols={{ base: 1, md: 2, lg: 3 }}>
* <Card>Item 1</Card>
* <Card>Item 2</Card>
* <Card>Item 3</Card>
* </Grid>
* ```
*/
export function Grid({
cols = { base: 1, md: 2, lg: 3 },
gap = 6,
className,
children,
...props
}: {
cols?: { base?: number; sm?: number; md?: number; lg?: number; xl?: number };
gap?: number;
className?: string;
children: React.ReactNode;
}) {
const gridCols = {
base: `grid-cols-${cols.base ?? 1}`,
sm: cols.sm ? `sm:grid-cols-${cols.sm}` : '',
md: cols.md ? `md:grid-cols-${cols.md}` : '',
lg: cols.lg ? `lg:grid-cols-${cols.lg}` : '',
xl: cols.xl ? `xl:grid-cols-${cols.xl}` : ''
};
return (
<div
className={cn(
'grid',
gridCols.base,
gridCols.sm,
gridCols.md,
gridCols.lg,
gridCols.xl,
`gap-${gap}`,
className
)}
{...props}
>
{children}
</div>
);
}
AI assistants use these components instead of inventing responsive patterns:
// AI-generated with responsive components
<Container>
<Grid cols={{ base: 1, md: 2, lg: 3 }}>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</Grid>
</Container>
Form Styling Patterns
Forms require consistent styling and error handling. Create a form field wrapper:
// components/ui/form-field.tsx
export interface FormFieldProps {
/**
* Field label (required for accessibility)
*/
label: string;
/**
* Error message - shows error state when present
*/
error?: string;
/**
* Helper text shown below input
*/
hint?: string;
/**
* Required field indicator
*/
required?: boolean;
/**
* Field ID (auto-generated if not provided)
*/
id?: string;
children: React.ReactNode;
}
/**
* FormField - Wrapper for form inputs with label and error handling
*
* @example
* ```tsx
* <form className="space-y-6">
* <FormField label="Email" error={errors.email} required>
* <Input type="email" name="email" />
* </FormField>
*
* <FormField label="Password" hint="Minimum 8 characters">
* <Input type="password" name="password" />
* </FormField>
* </form>
* ```
*/
export function FormField({
label,
error,
hint,
required,
id: providedId,
children
}: FormFieldProps) {
const id = providedId ?? useId();
return (
<div className="space-y-2">
<label
htmlFor={id}
className="block text-sm font-medium text-primary"
>
{label}
{required && <span className="text-destructive-default ml-1">*</span>}
</label>
<div>
{React.cloneElement(children as React.ReactElement, {
id,
'aria-invalid': !!error,
'aria-describedby': error ? `${id}-error` : hint ? `${id}-hint` : undefined
})}
</div>
{error && (
<p id={`${id}-error`} className="text-sm text-destructive-default">
{error}
</p>
)}
{hint && !error && (
<p id={`${id}-hint`} className="text-sm text-secondary">
{hint}
</p>
)}
</div>
);
}
AI-generated forms now have consistent styling:
// AI automatically uses FormField wrapper
function SignUpForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
return (
<form className="space-y-6" onSubmit={handleSubmit}>
<FormField label="Full Name" error={errors.name} required>
<Input name="name" />
</FormField>
<FormField label="Email" error={errors.email} required>
<Input type="email" name="email" />
</FormField>
<FormField
label="Password"
error={errors.password}
hint="Minimum 8 characters, include number and symbol"
required
>
<Input type="password" name="password" />
</FormField>
<Button variant="primary" type="submit" className="w-full">
Create Account
</Button>
</form>
);
}
Dark Mode Support
Implement theme-aware styling with CSS variables:
/* styles/themes.css */
:root {
/* Light mode tokens */
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-bg-base: #FFFFFF;
--color-bg-raised: #F9FAFB;
--color-action-primary-default: #0066FF;
/* ...rest of light tokens */
}
[data-theme="dark"] {
/* Dark mode tokens */
--color-text-primary: #F9FAFB;
--color-text-secondary: #9CA3AF;
--color-bg-base: #111827;
--color-bg-raised: #1F2937;
--color-action-primary-default: #3B82F6;
/* ...rest of dark tokens */
}
Components automatically adapt:
// No theme-specific code needed in components
<div className="bg-base text-primary">
{/* Automatically switches between light/dark */}
</div>
Theme provider:
// components/theme-provider.tsx
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Testing AI-Generated Styling
Automate checks for style consistency:
// tests/style-compliance.test.ts
import { render } from '@testing-library/react';
import { Button } from '@/components/ui/button';
describe('Button styling compliance', () => {
it('uses design tokens, not arbitrary colors', () => {
const { container } = render(<Button>Click me</Button>);
const button = container.firstChild as HTMLElement;
// Get computed styles
const classes = button.className;
// Should use token-based classes
expect(classes).toMatch(/bg-action-primary/);
// Should NOT use arbitrary colors
expect(classes).not.toMatch(/bg-blue-\d{3}/);
expect(classes).not.toMatch(/bg-\[#[0-9A-Fa-f]{6}\]/);
});
it('uses variant prop instead of custom className', () => {
const { container } = render(
<Button variant="destructive">Delete</Button>
);
const button = container.firstChild as HTMLElement;
expect(button.className).toMatch(/bg-action-destructive/);
});
});
Visual regression testing:
// tests/visual-regression.spec.ts
import { test, expect } from '@playwright/test';
test('button variants match design system', async ({ page }) => {
await page.goto('/storybook?path=/story/button--all-variants');
await expect(page).toHaveScreenshot('button-variants.png', {
maxDiffPixels: 100
});
});
Real-World Integration
FramingUI provides pre-built, AI-optimized component styling:
import { Button, Card, FormField, Input } from '@framingui/components';
// Components are already styled with design tokens
// AI assistants understand the variant system
// Dark mode works automatically
function UserProfile() {
return (
<Card>
<CardHeader>
<CardTitle>Edit Profile</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4">
<FormField label="Display Name">
<Input />
</FormField>
<Button variant="primary">Save</Button>
</form>
</CardContent>
</Card>
);
}
The AI generates this code because FramingUI's components are documented with usage examples, variant props are clearly defined, and design tokens are structured for AI pattern matching.
Conclusion
AI-assisted React component styling works when your codebase provides machine-readable styling context. Design tokens with semantic naming, variant-based component APIs, composition patterns, and usage examples train AI assistants to generate styled components that match your design system.
Start by defining semantic color and spacing tokens. Build variant-based components using CVA or similar libraries. Document composition patterns with code examples. Configure AI assistants to reference your component library. Add automated tests to catch style violations.
The workflow isn't about restricting AI—it's about giving it better inputs. When styling is structured as variants and tokens rather than arbitrary classes, AI-generated components are consistent, maintainable, and design-system-compliant from the first generation.