Best Practices

AI Rules for Design System Components

AI creates one-off components with hardcoded styles and no design tokens. Rules for token-based theming, variant APIs, accessible primitives, Storybook documentation, and component API design.

8 min read·February 14, 2025

Ten developers, ten different button styles — hardcoded colors, inconsistent APIs, no documentation

Design tokens, cva variant APIs, accessible primitives, Storybook docs, consistent prop patterns

AI Creates Components That Cannot Scale

AI generates components with: hardcoded colors (bg-blue-500 scattered across every button), inconsistent APIs (onClick in one component, onPress in another, handleClick in a third), no variants (separate PrimaryButton, SecondaryButton, DangerButton components instead of one Button with variants), no accessibility built in (missing ARIA attributes, no keyboard handling), and no documentation (component usage is tribal knowledge). Ten developers create ten different button styles because there is no system.

Modern design systems are: token-based (colors, spacing, typography defined as tokens, not hardcoded values), variant-driven (one Button component with variant, size, and state props), accessible by default (ARIA attributes, keyboard navigation, focus management built into primitives), documented (Storybook with props table, examples, and usage guidelines), and API-consistent (same prop patterns across all components). AI generates none of these.

These rules cover: design token systems, variant APIs with cva, accessible primitive components, Storybook-driven documentation, and consistent component API patterns.

Rule 1: Design Token System

The rule: 'Define all visual values as design tokens: colors (--color-primary-500), spacing (--space-4: 1rem), typography (--font-size-lg: 1.125rem), border radius (--radius-md: 0.375rem), and shadows (--shadow-md). Components reference tokens, never raw values. Tailwind CSS 4: define tokens in the @theme directive. Token changes propagate to every component instantly — rebrand by changing 20 tokens, not 200 className strings.'

For token naming: 'Use a scale-based naming convention: --color-primary-50 through --color-primary-950 (light to dark), --space-0 through --space-16 (0 to 4rem), --font-size-xs through --font-size-4xl. Scale names are: intuitive (larger number = larger value), extensible (add --space-20 without renaming), and framework-aligned (matches Tailwind default scale). Semantic aliases: --color-bg maps to --color-gray-50 in light mode, --color-gray-950 in dark mode.'

AI generates: className="bg-blue-500 text-white px-4 py-2 rounded-md" on every button. The designer changes the primary color from blue to indigo. Developer finds and replaces bg-blue-500 across 47 files. With tokens: change --color-primary-500 once. Every button updates. One change, zero find-and-replace.

  • Tokens for: colors, spacing, typography, radius, shadows — never raw values in components
  • Scale naming: --color-primary-50 to --color-primary-950, --space-0 to --space-16
  • Semantic aliases: --color-bg, --color-text — map to scale tokens per theme
  • Tailwind CSS 4: @theme directive for token definition — classes generated from tokens
  • Rebrand = change 20 tokens, not 200 classNames across 50 files
💡 20 Tokens, Not 200 ClassNames

Designer changes primary color from blue to indigo. Without tokens: find and replace bg-blue-500 across 47 files. With tokens: change --color-primary-500 once. Every button, link, and badge updates instantly. One change, zero find-and-replace.

Rule 2: Variant APIs with cva

The rule: 'Use class-variance-authority (cva) or a similar library to define component variants declaratively: const button = cva("inline-flex items-center font-medium", { variants: { variant: { primary: "bg-primary-500 text-white", secondary: "bg-gray-100 text-gray-900", danger: "bg-red-500 text-white" }, size: { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2 text-base", lg: "px-6 py-3 text-lg" } }, defaultVariants: { variant: "primary", size: "md" } }). Usage: <Button variant="danger" size="lg">Delete</Button>.'

For compound variants: 'cva supports compound variants for combinations: compoundVariants: [{ variant: "primary", size: "lg", className: "shadow-lg" }]. This applies shadow-lg only when variant is primary AND size is lg. Compound variants handle edge cases without conditional logic in the component — the variant system is declarative and complete.'

AI generates: function PrimaryButton, function SecondaryButton, function DangerButton, function SmallButton, function LargeButton — five components for one button. Or: className={variant === 'primary' ? 'bg-blue-500' : variant === 'secondary' ? 'bg-gray-100' : 'bg-red-500'} — nested ternaries. cva: one component, declarative variants, type-safe props, zero ternaries.

Rule 3: Accessible Primitive Components

The rule: 'Build on accessible primitive libraries: Radix UI, React Aria, or Headless UI. These provide: keyboard navigation (arrow keys in menus, Escape to close), focus management (trap focus in modals, return focus on close), ARIA attributes (role, aria-expanded, aria-selected), and screen reader announcements (live regions for dynamic content). Your design system skins these primitives with your tokens and variants — you own the visual layer, the library owns the interaction layer.'

For the build vs buy decision: 'Building accessible components from scratch requires: WAI-ARIA Authoring Practices knowledge, keyboard interaction patterns per component type, focus management logic, screen reader testing across multiple readers (NVDA, VoiceOver, JAWS), and ongoing maintenance as standards evolve. Radix UI provides all of this — tested, maintained, and specification-compliant. Style the primitives; do not reimplement the interaction logic.'

AI generates: <div onClick={toggle}>{isOpen && <div>{children}</div>}</div> — a dropdown with no keyboard support, no ARIA, no focus management, and a div instead of a button (not keyboard-focusable). Radix DropdownMenu: arrow key navigation, Escape to close, focus trapped when open, aria-expanded, screen reader compatible. Same visual, completely different accessibility.

  • Radix UI / React Aria / Headless UI for interaction primitives
  • Keyboard: arrow keys, Escape, Enter/Space — per WAI-ARIA Authoring Practices
  • Focus management: trap in modals, return on close, skip links
  • ARIA: role, aria-expanded, aria-selected, aria-live for dynamic content
  • Style the primitive — do not reimplement keyboard and focus logic
⚠️ div onClick vs Radix Dropdown

AI builds a dropdown with <div onClick>: no keyboard support, no ARIA, no focus management. Radix DropdownMenu: arrow keys, Escape to close, focus trap, aria-expanded, screen reader support. Same visual appearance, completely different accessibility.

Rule 4: Storybook-Driven Component Documentation

The rule: 'Every design system component has a Storybook story with: a default story (component with default props), variant stories (one story per variant combination), interactive stories (args controls for live prop editing), edge case stories (long text, empty state, error state, loading state), and usage documentation (MDX page with guidelines, do/don't examples, and accessibility notes). Storybook is the component catalog — developers browse, test, and copy-paste from it.'

For story structure: 'Default export: component metadata (title, component, argTypes). Named exports: one per story. Pattern: export const Primary: Story = { args: { variant: "primary", children: "Button" } }; export const AllVariants: Story = { render: () => (<div className="flex gap-4">{["primary", "secondary", "danger"].map(v => <Button variant={v}>{v}</Button>)}</div>) }. The AllVariants story is a visual regression test — every variant visible at once.'

AI generates: components with no documentation. A new developer joins: 'How do I use the Button component? What variants exist? What props does it accept?' Without Storybook: read the source code, guess, try, fail. With Storybook: browse the catalog, see every variant, edit props live, copy the code. Onboarding time drops from hours to minutes.

ℹ️ Onboarding: Hours to Minutes

Without Storybook: new developer reads source code, guesses prop names, tries, fails. With Storybook: browse the catalog, see every variant live, edit props interactively, copy the code. Component discovery goes from archeology to browsing.

Rule 5: Consistent Component API Patterns

The rule: 'Establish and enforce API conventions across all components: (1) variant for visual style (primary, secondary, danger), (2) size for dimension scale (sm, md, lg), (3) disabled for interaction state (boolean), (4) className for style extension (merged with defaults via cn/clsx), (5) asChild for polymorphic rendering (Radix pattern). Every component follows the same patterns — learning one component teaches you all components.'

For prop forwarding: 'Extend the native HTML element props: type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof button>. This means: every native button prop (type, disabled, onClick, form, autoFocus) works automatically. The design system component is a superset of the native element — it adds variant and size but preserves everything the native element supports.'

AI generates: Button with onClick, Input with handleChange, Modal with onDismiss, Dropdown with onToggle — four components, four different naming conventions for the same concept (event callbacks). Consistent API: onChange everywhere, onClose for dismissal, onClick for actions. One convention, zero cognitive load switching between components.

Complete Design System Rules Template

Consolidated rules for design system components.

  • Design tokens for all visual values — colors, spacing, typography, radius, shadows
  • cva for variant APIs — declarative variants, compound variants, type-safe props
  • Accessible primitives (Radix, React Aria) — style the visual, library owns interaction
  • Storybook for every component — variants, edge cases, interactive controls, usage docs
  • Consistent API: variant, size, disabled, className, asChild across all components
  • Extend native HTML props — ButtonHTMLAttributes, design system adds variant/size
  • One Button with variants — not PrimaryButton, SecondaryButton, DangerButton
  • cn/clsx for className merging — consumer styles merge with, not override, defaults
AI Rules for Design System Components — RuleSync Blog