AI for Front-End Component Libraries: React, Vue, Svelte & Storybook

Building and maintaining a component library is one of the most time-intensive endeavors in front-end development. From designing consistent APIs to writing comprehensive documentation, the effort required often discourages teams from creating the reusable component systems that would ultimately save them countless hours. Enter AI-assisted development, which is transforming how we approach component library creation.

In this comprehensive guide, we'll explore how to leverage AI tools like GitHub Copilot, ChatGPT, and Claude to generate production-quality components for React, Vue, and Svelte. You'll learn structured prompting techniques, see real code examples, and discover strategies for maintaining consistency, accessibility, and quality across your entire component library.

The AI-Assisted Component Development Workflow

Before diving into framework-specific examples, let's establish a workflow that maximizes AI effectiveness for component library development. The key is treating AI as a pair programmer who needs clear context rather than a magic code generator.

Structured Prompting for Components

The quality of AI-generated components depends heavily on prompt structure. Here's a template that consistently produces production-ready results:

// Component Generation Prompt Template
`Generate a [Component Name] component with the following specifications:

Framework: [React/Vue/Svelte] with TypeScript
Styling: [Tailwind CSS/CSS Modules/Styled Components]

Component Requirements:
- Purpose: [Clear description of component function]
- Variants: [List all visual variants]
- Sizes: [sm, md, lg or custom sizes]
- States: [default, hover, focus, disabled, loading, error]

Props Interface:
- [propName]: [type] - [description] - [required/optional]

Accessibility Requirements:
- WCAG 2.1 AA compliance
- Keyboard navigation support
- Screen reader announcements
- Focus management

Include:
1. TypeScript interfaces for all props
2. JSDoc/TSDoc comments
3. Default prop values
4. Proper ARIA attributes
5. CSS custom properties for theming

Do not include:
- Inline styles (use CSS classes)
- Any dependencies
- Console.log statements`

Generating React Components with AI

React's component model and extensive documentation in AI training data make it ideal for AI-assisted development. Let's build a comprehensive Button component.

Building an Accessible Button Component

// components/Button/Button.tsx
import React, { forwardRef, ButtonHTMLAttributes, ReactNode } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

/**
 * Button component with multiple variants, sizes, and states.
 * Follows WCAG 2.1 AA accessibility guidelines.
 *
 * @example
 * ```tsx
 * <Button variant="primary" size="md">Click me</Button>
 * <Button variant="outline" isLoading>Loading...</Button>
 * ```
 */

const buttonVariants = cva(
  // Base styles applied to all buttons
  [
    'inline-flex items-center justify-center',
    'font-medium rounded-lg',
    'transition-colors duration-200',
    'focus-visible:outline-none focus-visible:ring-2',
    'focus-visible:ring-offset-2',
    'disabled:opacity-50 disabled:pointer-events-none',
  ],
  {
    variants: {
      variant: {
        primary: [
          'bg-primary-600 text-white',
          'hover:bg-primary-700',
          'focus-visible:ring-primary-500',
        ],
        secondary: [
          'bg-secondary-100 text-secondary-900',
          'hover:bg-secondary-200',
          'focus-visible:ring-secondary-500',
        ],
        outline: [
          'border-2 border-current',
          'bg-transparent',
          'hover:bg-gray-100 dark:hover:bg-gray-800',
          'focus-visible:ring-gray-500',
        ],
        ghost: [
          'bg-transparent',
          'hover:bg-gray-100 dark:hover:bg-gray-800',
          'focus-visible:ring-gray-500',
        ],
        destructive: [
          'bg-red-600 text-white',
          'hover:bg-red-700',
          'focus-visible:ring-red-500',
        ],
        link: [
          'text-primary-600 underline-offset-4',
          'hover:underline',
          'focus-visible:ring-primary-500',
        ],
      },
      size: {
        sm: 'h-8 px-3 text-sm gap-1.5',
        md: 'h-10 px-4 text-base gap-2',
        lg: 'h-12 px-6 text-lg gap-2.5',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  /** Button content */
  children: ReactNode;
  /** Shows loading spinner and disables interaction */
  isLoading?: boolean;
  /** Icon to display before text */
  leftIcon?: ReactNode;
  /** Icon to display after text */
  rightIcon?: ReactNode;
  /** Makes button full width */
  fullWidth?: boolean;
}

const LoadingSpinner = () => (
  <svg
    className="animate-spin h-4 w-4"
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    aria-hidden="true"
  >
    <circle
      className="opacity-25"
      cx="12"
      cy="12"
      r="10"
      stroke="currentColor"
      strokeWidth="4"
    />
    <path
      className="opacity-75"
      fill="currentColor"
      d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
    />
  </svg>
);

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      variant,
      size,
      children,
      isLoading = false,
      leftIcon,
      rightIcon,
      fullWidth = false,
      disabled,
      ...props
    },
    ref
  ) => {
    return (
      <button
        ref={ref}
        className={cn(
          buttonVariants({ variant, size }),
          fullWidth && 'w-full',
          className
        )}
        disabled={disabled || isLoading}
        aria-busy={isLoading}
        aria-disabled={disabled || isLoading}
        {...props}
      >
        {isLoading ? (
          <>
            <LoadingSpinner />
            <span className="sr-only">Loading</span>
            {children}
          </>
        ) : (
          <>
            {leftIcon && <span aria-hidden="true">{leftIcon}</span>}
            {children}
            {rightIcon && <span aria-hidden="true">{rightIcon}</span>}
          </>
        )}
      </button>
    );
  }
);

Button.displayName = 'Button';

export { buttonVariants };

Building a Form Input Component

Form components require careful attention to accessibility. Here's an AI-generated Input component with comprehensive features:

// components/Input/Input.tsx
import React, { forwardRef, InputHTMLAttributes, useId } from 'react';
import { cn } from '@/lib/utils';

export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  /** Label text for the input */
  label?: string;
  /** Helper text displayed below input */
  helperText?: string;
  /** Error message - puts input in error state */
  error?: string;
  /** Left addon element (icon or text) */
  leftAddon?: React.ReactNode;
  /** Right addon element (icon or text) */
  rightAddon?: React.ReactNode;
  /** Makes the input full width */
  fullWidth?: boolean;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  (
    {
      className,
      type = 'text',
      label,
      helperText,
      error,
      leftAddon,
      rightAddon,
      fullWidth = false,
      disabled,
      required,
      id: providedId,
      ...props
    },
    ref
  ) => {
    const generatedId = useId();
    const id = providedId || generatedId;
    const helperId = `${id}-helper`;
    const errorId = `${id}-error`;

    const hasError = Boolean(error);
    const describedBy = [
      helperText && !hasError ? helperId : null,
      hasError ? errorId : null,
    ]
      .filter(Boolean)
      .join(' ') || undefined;

    return (
      <div className={cn('flex flex-col gap-1.5', fullWidth && 'w-full')}>
        {label && (
          <label
            htmlFor={id}
            className={cn(
              'text-sm font-medium text-gray-700 dark:text-gray-300',
              disabled && 'opacity-50'
            )}
          >
            {label}
            {required && (
              <span className="text-red-500 ml-1" aria-hidden="true">
                *
              </span>
            )}
          </label>
        )}

        <div className="relative flex items-center">
          {leftAddon && (
            <div className="absolute left-3 text-gray-500" aria-hidden="true">
              {leftAddon}
            </div>
          )}

          <input
            ref={ref}
            id={id}
            type={type}
            disabled={disabled}
            required={required}
            aria-invalid={hasError}
            aria-describedby={describedBy}
            className={cn(
              // Base styles
              'flex h-10 w-full rounded-lg border px-3 py-2',
              'text-sm placeholder:text-gray-400',
              'transition-colors duration-200',
              // Focus styles
              'focus:outline-none focus:ring-2 focus:ring-offset-0',
              // Default state
              !hasError && [
                'border-gray-300 dark:border-gray-600',
                'bg-white dark:bg-gray-900',
                'focus:border-primary-500 focus:ring-primary-500/20',
              ],
              // Error state
              hasError && [
                'border-red-500',
                'bg-red-50 dark:bg-red-900/10',
                'focus:border-red-500 focus:ring-red-500/20',
              ],
              // Disabled state
              disabled && 'opacity-50 cursor-not-allowed bg-gray-100',
              // Addon padding
              leftAddon && 'pl-10',
              rightAddon && 'pr-10',
              className
            )}
            {...props}
          />

          {rightAddon && (
            <div className="absolute right-3 text-gray-500" aria-hidden="true">
              {rightAddon}
            </div>
          )}
        </div>

        {helperText && !hasError && (
          <p id={helperId} className="text-sm text-gray-500">
            {helperText}
          </p>
        )}

        {hasError && (
          <p id={errorId} className="text-sm text-red-600" role="alert">
            {error}
          </p>
        )}
      </div>
    );
  }
);

Input.displayName = 'Input';

Generating Vue Components with AI

Vue's Composition API and single-file components work well with AI generation. Here's how to create reusable Vue components:

Vue Modal Component

<!-- components/Modal/Modal.vue -->
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';

/**
 * Accessible modal dialog component with focus trapping
 * and keyboard navigation support.
 */

export interface ModalProps {
  /** Controls modal visibility */
  modelValue: boolean;
  /** Modal title for accessibility */
  title: string;
  /** Size variant */
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
  /** Allow closing by clicking backdrop */
  closeOnBackdrop?: boolean;
  /** Allow closing with Escape key */
  closeOnEscape?: boolean;
  /** Show close button */
  showCloseButton?: boolean;
}

const props = withDefaults(defineProps<ModalProps>(), {
  size: 'md',
  closeOnBackdrop: true,
  closeOnEscape: true,
  showCloseButton: true,
});

const emit = defineEmits<{
  'update:modelValue': [value: boolean];
  close: [];
}>();

const modalRef = ref<HTMLElement | null>(null);
const previousActiveElement = ref<HTMLElement | null>(null);

const sizeClasses = {
  sm: 'max-w-sm',
  md: 'max-w-md',
  lg: 'max-w-lg',
  xl: 'max-w-xl',
  full: 'max-w-full mx-4',
};

const close = () => {
  emit('update:modelValue', false);
  emit('close');
};

const handleBackdropClick = (event: MouseEvent) => {
  if (props.closeOnBackdrop && event.target === event.currentTarget) {
    close();
  }
};

const handleKeydown = (event: KeyboardEvent) => {
  if (props.closeOnEscape && event.key === 'Escape') {
    close();
  }

  // Focus trap
  if (event.key === 'Tab' && modalRef.value) {
    const focusableElements = modalRef.value.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const firstElement = focusableElements[0] as HTMLElement;
    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

    if (event.shiftKey && document.activeElement === firstElement) {
      event.preventDefault();
      lastElement?.focus();
    } else if (!event.shiftKey && document.activeElement === lastElement) {
      event.preventDefault();
      firstElement?.focus();
    }
  }
};

watch(
  () => props.modelValue,
  async (isOpen) => {
    if (isOpen) {
      previousActiveElement.value = document.activeElement as HTMLElement;
      document.body.style.overflow = 'hidden';
      await nextTick();
      modalRef.value?.focus();
    } else {
      document.body.style.overflow = '';
      previousActiveElement.value?.focus();
    }
  }
);

onMounted(() => {
  document.addEventListener('keydown', handleKeydown);
});

onUnmounted(() => {
  document.removeEventListener('keydown', handleKeydown);
  document.body.style.overflow = '';
});
</script>

<template>
  <Teleport to="body">
    <Transition
      enter-active-class="transition-opacity duration-200"
      leave-active-class="transition-opacity duration-200"
      enter-from-class="opacity-0"
      leave-to-class="opacity-0"
    >
      <div
        v-if="modelValue"
        class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
        @click="handleBackdropClick"
        aria-labelledby="modal-title"
        role="dialog"
        aria-modal="true"
      >
        <Transition
          enter-active-class="transition-all duration-200"
          leave-active-class="transition-all duration-200"
          enter-from-class="opacity-0 scale-95"
          leave-to-class="opacity-0 scale-95"
        >
          <div
            v-if="modelValue"
            ref="modalRef"
            :class="[
              'relative w-full bg-white dark:bg-gray-800 rounded-xl shadow-xl',
              'max-h-[90vh] overflow-auto',
              sizeClasses[size],
            ]"
            tabindex="-1"
          >
            <!-- Header -->
            <div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
              <h2
                id="modal-title"
                class="text-lg font-semibold text-gray-900 dark:text-white"
              >
                {{ title }}
              </h2>
              <button
                v-if="showCloseButton"
                type="button"
                class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
                aria-label="Close modal"
                @click="close"
              >
                <svg
                  class="w-5 h-5"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M6 18L18 6M6 6l12 12"
                  />
                </svg>
              </button>
            </div>

            <!-- Content -->
            <div class="p-4">
              <slot />
            </div>

            <!-- Footer -->
            <div
              v-if="$slots.footer"
              class="flex justify-end gap-3 p-4 border-t dark:border-gray-700"
            >
              <slot name="footer" />
            </div>
          </div>
        </Transition>
      </div>
    </Transition>
  </Teleport>
</template>

Generating Svelte Components with AI

Svelte's reactive declarations and compile-time approach create lean, performant components. Here's an AI-generated Accordion component:

Svelte Accordion Component

<!-- components/Accordion/Accordion.svelte -->
<script lang="ts">
  import { slide } from 'svelte/transition';
  import { createEventDispatcher } from 'svelte';

  /**
   * Accessible accordion component with keyboard navigation.
   * Supports single or multiple open panels.
   */

  export interface AccordionItem {
    id: string;
    title: string;
    content: string;
    disabled?: boolean;
  }

  export let items: AccordionItem[] = [];
  export let allowMultiple = false;
  export let defaultOpenIds: string[] = [];

  const dispatch = createEventDispatcher<{
    change: { openIds: string[] };
  }>();

  let openIds: Set<string> = new Set(defaultOpenIds);

  function toggleItem(id: string) {
    const item = items.find((i) => i.id === id);
    if (item?.disabled) return;

    if (allowMultiple) {
      if (openIds.has(id)) {
        openIds.delete(id);
      } else {
        openIds.add(id);
      }
      openIds = openIds;
    } else {
      openIds = openIds.has(id) ? new Set() : new Set([id]);
    }

    dispatch('change', { openIds: Array.from(openIds) });
  }

  function handleKeydown(event: KeyboardEvent, index: number) {
    const enabledItems = items.filter((item) => !item.disabled);
    const currentEnabledIndex = enabledItems.findIndex(
      (item) => item.id === items[index].id
    );

    let targetIndex: number | null = null;

    switch (event.key) {
      case 'ArrowDown':
        event.preventDefault();
        targetIndex = (currentEnabledIndex + 1) % enabledItems.length;
        break;
      case 'ArrowUp':
        event.preventDefault();
        targetIndex =
          (currentEnabledIndex - 1 + enabledItems.length) % enabledItems.length;
        break;
      case 'Home':
        event.preventDefault();
        targetIndex = 0;
        break;
      case 'End':
        event.preventDefault();
        targetIndex = enabledItems.length - 1;
        break;
    }

    if (targetIndex !== null) {
      const targetId = enabledItems[targetIndex].id;
      const button = document.getElementById(`accordion-trigger-${targetId}`);
      button?.focus();
    }
  }

  $: isOpen = (id: string) => openIds.has(id);
</script>

<div class="divide-y divide-gray-200 dark:divide-gray-700 border rounded-lg dark:border-gray-700">
  {#each items as item, index (item.id)}
    <div class="accordion-item">
      <h3>
        <button
          id="accordion-trigger-{item.id}"
          type="button"
          class="flex items-center justify-between w-full p-4 text-left font-medium
            text-gray-900 dark:text-white
            hover:bg-gray-50 dark:hover:bg-gray-800
            focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500
            disabled:opacity-50 disabled:cursor-not-allowed
            transition-colors duration-200"
          aria-expanded={isOpen(item.id)}
          aria-controls="accordion-panel-{item.id}"
          disabled={item.disabled}
          on:click={() => toggleItem(item.id)}
          on:keydown={(e) => handleKeydown(e, index)}
        >
          <span>{item.title}</span>
          <svg
            class="w-5 h-5 transition-transform duration-200"
            class:rotate-180={isOpen(item.id)}
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
            aria-hidden="true"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M19 9l-7 7-7-7"
            />
          </svg>
        </button>
      </h3>

      {#if isOpen(item.id)}
        <div
          id="accordion-panel-{item.id}"
          role="region"
          aria-labelledby="accordion-trigger-{item.id}"
          transition:slide={{ duration: 200 }}
        >
          <div class="p-4 text-gray-600 dark:text-gray-300">
            {item.content}
          </div>
        </div>
      {/if}
    </div>
  {/each}
</div>

Creating Storybook Stories with AI

Storybook is essential for component library documentation. AI can generate comprehensive stories that showcase all component variants and states.

Button Component Stories

// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { Mail, ArrowRight, Download } from 'lucide-react';

/**
 * The Button component is the primary interactive element
 * for user actions. It supports multiple variants, sizes,
 * and states for different use cases.
 */
const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  parameters: {
    layout: 'centered',
    docs: {
      description: {
        component:
          'A versatile button component with multiple variants, sizes, and accessibility features.',
      },
    },
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'outline', 'ghost', 'destructive', 'link'],
      description: 'Visual style variant',
      table: {
        defaultValue: { summary: 'primary' },
      },
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg', 'icon'],
      description: 'Button size',
      table: {
        defaultValue: { summary: 'md' },
      },
    },
    isLoading: {
      control: 'boolean',
      description: 'Shows loading spinner',
    },
    disabled: {
      control: 'boolean',
      description: 'Disables button interaction',
    },
    fullWidth: {
      control: 'boolean',
      description: 'Makes button full width of container',
    },
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

// Primary variant - default button style
export const Primary: Story = {
  args: {
    children: 'Primary Button',
    variant: 'primary',
  },
};

// All variants showcase
export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-wrap gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="link">Link</Button>
    </div>
  ),
  parameters: {
    docs: {
      description: {
        story: 'All available button variants for different use cases.',
      },
    },
  },
};

// Size variations
export const Sizes: Story = {
  render: () => (
    <div className="flex items-center gap-4">
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
};

// With icons
export const WithIcons: Story = {
  render: () => (
    <div className="flex flex-wrap gap-4">
      <Button leftIcon={<Mail className="w-4 h-4" />}>
        Send Email
      </Button>
      <Button rightIcon={<ArrowRight className="w-4 h-4" />}>
        Continue
      </Button>
      <Button
        leftIcon={<Download className="w-4 h-4" />}
        rightIcon={<ArrowRight className="w-4 h-4" />}
      >
        Download
      </Button>
    </div>
  ),
};

// Loading state
export const Loading: Story = {
  args: {
    children: 'Processing...',
    isLoading: true,
  },
  parameters: {
    docs: {
      description: {
        story: 'Loading state shows a spinner and prevents interaction.',
      },
    },
  },
};

// Disabled state
export const Disabled: Story = {
  args: {
    children: 'Disabled Button',
    disabled: true,
  },
};

// Full width
export const FullWidth: Story = {
  args: {
    children: 'Full Width Button',
    fullWidth: true,
  },
  decorators: [
    (Story) => (
      <div className="w-80">
        <Story />
      </div>
    ),
  ],
};

// Icon only button
export const IconOnly: Story = {
  args: {
    children: <Mail className="w-5 h-5" />,
    size: 'icon',
    'aria-label': 'Send email',
  },
};

// Interactive playground
export const Playground: Story = {
  args: {
    children: 'Click Me',
    variant: 'primary',
    size: 'md',
    isLoading: false,
    disabled: false,
    fullWidth: false,
  },
};

AI-Assisted Accessibility Implementation

Accessibility often gets overlooked in AI-generated components. Here's a prompt strategy and testing approach that ensures WCAG compliance:

Accessibility-First Prompting

// Accessibility-focused component prompt
`Generate an accessible [Component] that MUST include:

WCAG 2.1 AA Requirements:
1. Color contrast ratio of at least 4.5:1 for text
2. Touch targets minimum 44x44 pixels
3. Focus indicators visible in all states
4. No information conveyed by color alone

Keyboard Navigation:
- All interactive elements focusable with Tab
- Logical focus order
- Visible focus indicators
- Enter/Space to activate
- Escape to close/cancel
- Arrow keys for navigation within widgets

Screen Reader Support:
- Descriptive labels for all interactive elements
- State announcements (expanded, selected, checked)
- Error announcements using role="alert"
- Progress announcements for loading states
- Landmark regions where appropriate

ARIA Implementation:
- aria-label for icon-only buttons
- aria-describedby for form field help text
- aria-invalid and aria-errormessage for errors
- aria-expanded for collapsible content
- aria-live for dynamic content updates

Include:
- axe-core test configuration
- Keyboard interaction tests
- Screen reader testing notes`

Component Testing with AI

// components/Button/__tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from '../Button';

expect.extend(toHaveNoViolations);

describe('Button', () => {
  describe('Rendering', () => {
    it('renders children correctly', () => {
      render(<Button>Click me</Button>);
      expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
    });

    it('applies variant classes correctly', () => {
      const { rerender } = render(<Button variant="primary">Test</Button>);
      expect(screen.getByRole('button')).toHaveClass('bg-primary-600');

      rerender(<Button variant="destructive">Test</Button>);
      expect(screen.getByRole('button')).toHaveClass('bg-red-600');
    });

    it('applies size classes correctly', () => {
      const { rerender } = render(<Button size="sm">Test</Button>);
      expect(screen.getByRole('button')).toHaveClass('h-8');

      rerender(<Button size="lg">Test</Button>);
      expect(screen.getByRole('button')).toHaveClass('h-12');
    });
  });

  describe('Interactions', () => {
    it('calls onClick when clicked', async () => {
      const handleClick = jest.fn();
      render(<Button onClick={handleClick}>Click me</Button>);

      await userEvent.click(screen.getByRole('button'));
      expect(handleClick).toHaveBeenCalledTimes(1);
    });

    it('does not call onClick when disabled', async () => {
      const handleClick = jest.fn();
      render(
        <Button onClick={handleClick} disabled>
          Click me
        </Button>
      );

      await userEvent.click(screen.getByRole('button'));
      expect(handleClick).not.toHaveBeenCalled();
    });

    it('does not call onClick when loading', async () => {
      const handleClick = jest.fn();
      render(
        <Button onClick={handleClick} isLoading>
          Click me
        </Button>
      );

      await userEvent.click(screen.getByRole('button'));
      expect(handleClick).not.toHaveBeenCalled();
    });
  });

  describe('Loading State', () => {
    it('shows loading spinner when isLoading is true', () => {
      render(<Button isLoading>Loading</Button>);

      expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
      expect(screen.getByText('Loading', { selector: '.sr-only' })).toBeInTheDocument();
    });

    it('disables button when loading', () => {
      render(<Button isLoading>Loading</Button>);
      expect(screen.getByRole('button')).toBeDisabled();
    });
  });

  describe('Accessibility', () => {
    it('has no accessibility violations', async () => {
      const { container } = render(<Button>Accessible Button</Button>);
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });

    it('has no accessibility violations in disabled state', async () => {
      const { container } = render(<Button disabled>Disabled Button</Button>);
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });

    it('has no accessibility violations in loading state', async () => {
      const { container } = render(<Button isLoading>Loading Button</Button>);
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });

    it('is keyboard accessible', async () => {
      const handleClick = jest.fn();
      render(<Button onClick={handleClick}>Press Enter</Button>);

      const button = screen.getByRole('button');
      button.focus();
      expect(button).toHaveFocus();

      await userEvent.keyboard('{Enter}');
      expect(handleClick).toHaveBeenCalledTimes(1);

      await userEvent.keyboard(' ');
      expect(handleClick).toHaveBeenCalledTimes(2);
    });

    it('has visible focus indicator', () => {
      render(<Button>Focus me</Button>);
      const button = screen.getByRole('button');
      button.focus();

      // Check for focus-visible ring classes
      expect(button).toHaveClass('focus-visible:ring-2');
    });
  });

  describe('Forwarding refs', () => {
    it('forwards ref to button element', () => {
      const ref = React.createRef<HTMLButtonElement>();
      render(<Button ref={ref}>Ref Button</Button>);

      expect(ref.current).toBeInstanceOf(HTMLButtonElement);
      expect(ref.current?.tagName).toBe('BUTTON');
    });
  });
});

Building Design System Components

A well-structured design system requires consistent tokens, patterns, and documentation. Here's how to use AI to create a cohesive system:

Design Tokens Configuration

// design-tokens/tokens.ts
/**
 * Design tokens for the component library.
 * These values are used across all components for consistency.
 */

export const tokens = {
  colors: {
    primary: {
      50: '#f0f9ff',
      100: '#e0f2fe',
      200: '#bae6fd',
      300: '#7dd3fc',
      400: '#38bdf8',
      500: '#0ea5e9',
      600: '#0284c7',
      700: '#0369a1',
      800: '#075985',
      900: '#0c4a6e',
    },
    gray: {
      50: '#f9fafb',
      100: '#f3f4f6',
      200: '#e5e7eb',
      300: '#d1d5db',
      400: '#9ca3af',
      500: '#6b7280',
      600: '#4b5563',
      700: '#374151',
      800: '#1f2937',
      900: '#111827',
    },
    semantic: {
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
      info: '#3b82f6',
    },
  },
  spacing: {
    px: '1px',
    0: '0',
    0.5: '0.125rem',
    1: '0.25rem',
    1.5: '0.375rem',
    2: '0.5rem',
    2.5: '0.625rem',
    3: '0.75rem',
    3.5: '0.875rem',
    4: '1rem',
    5: '1.25rem',
    6: '1.5rem',
    8: '2rem',
    10: '2.5rem',
    12: '3rem',
    16: '4rem',
    20: '5rem',
    24: '6rem',
  },
  typography: {
    fontFamily: {
      sans: ['Inter', 'system-ui', 'sans-serif'],
      mono: ['JetBrains Mono', 'monospace'],
    },
    fontSize: {
      xs: ['0.75rem', { lineHeight: '1rem' }],
      sm: ['0.875rem', { lineHeight: '1.25rem' }],
      base: ['1rem', { lineHeight: '1.5rem' }],
      lg: ['1.125rem', { lineHeight: '1.75rem' }],
      xl: ['1.25rem', { lineHeight: '1.75rem' }],
      '2xl': ['1.5rem', { lineHeight: '2rem' }],
      '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
      '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
    },
    fontWeight: {
      normal: '400',
      medium: '500',
      semibold: '600',
      bold: '700',
    },
  },
  borderRadius: {
    none: '0',
    sm: '0.125rem',
    DEFAULT: '0.25rem',
    md: '0.375rem',
    lg: '0.5rem',
    xl: '0.75rem',
    '2xl': '1rem',
    full: '9999px',
  },
  shadows: {
    sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
    DEFAULT: '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)',
    xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
  },
  animation: {
    duration: {
      fast: '150ms',
      normal: '200ms',
      slow: '300ms',
    },
    easing: {
      easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
      easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
      easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
    },
  },
} as const;

export type Tokens = typeof tokens;

AI-Generated Component Documentation

Comprehensive documentation is crucial for component adoption. Here's a prompt template for generating docs:

// Generate documentation prompt
`Create comprehensive documentation for the [Component] including:

1. Overview
   - Brief description of component purpose
   - When to use this component
   - When NOT to use this component

2. Installation
   - npm/yarn install commands
   - Import statements

3. Usage Examples
   - Basic usage
   - All variants with code
   - Common patterns
   - Edge cases

4. Props Reference
   - Table with prop name, type, default, description
   - Required vs optional indicators

5. Accessibility
   - Keyboard shortcuts
   - Screen reader behavior
   - ARIA attributes used

6. Styling
   - CSS custom properties available
   - How to override styles
   - Theming integration

7. Related Components
   - Components that work well together
   - Alternative components for different use cases

Format as MDX for Storybook integration.`

Component Quality Metrics

Tracking component quality ensures your library maintains high standards over time:

Key Quality Metrics

  • Accessibility Score - axe-core audit results (aim for 100%)
  • Bundle Size - Track individual component sizes with bundlesize
  • Test Coverage - Maintain 90%+ coverage for all components
  • TypeScript Strictness - No any types, strict mode enabled
  • Documentation Coverage - All props documented with examples
  • Storybook Stories - All variants and states covered

Component Library Versioning

Proper versioning prevents breaking changes from disrupting consumers:

// package.json versioning configuration
{
  "name": "@your-org/component-library",
  "version": "1.0.0",
  "scripts": {
    "release": "semantic-release",
    "release:dry-run": "semantic-release --dry-run"
  },
  "release": {
    "branches": ["main"],
    "plugins": [
      "@semantic-release/commit-analyzer",
      "@semantic-release/release-notes-generator",
      "@semantic-release/changelog",
      "@semantic-release/npm",
      "@semantic-release/github"
    ]
  }
}

// Commit message conventions for semantic versioning
// fix: Bumps PATCH version (1.0.0 -> 1.0.1)
// feat: Bumps MINOR version (1.0.0 -> 1.1.0)
// BREAKING CHANGE: Bumps MAJOR version (1.0.0 -> 2.0.0)

Best Practices for AI-Generated Components

Key Recommendations

  • Always specify TypeScript - Request typed props and interfaces explicitly
  • Include accessibility from the start - Add WCAG requirements to every prompt
  • Request comprehensive tests - Ask for unit tests with accessibility checks
  • Provide design tokens - Include your token system in context
  • Generate Storybook stories - Create stories for all variants and states
  • Document as you go - Generate JSDoc comments and README content
  • Review generated code - AI output needs human verification
  • Maintain consistency - Use the same patterns across all components

Conclusion

AI-assisted component library development represents a significant productivity multiplier when approached systematically. By using structured prompts that specify framework, styling, accessibility requirements, and documentation needs upfront, you can generate production-quality components in a fraction of the traditional development time.

The key insight is that AI excels at generating the scaffolding, boilerplate, and common patterns that make up the bulk of component code. Your expertise remains essential for architectural decisions, design system consistency, edge case handling, and quality assurance. Use AI to accelerate the tedious parts while investing your time in the high-value decisions that make your component library exceptional.

In our next article, we'll explore Integrating AI into CI/CD Pipelines, where you'll learn how to automate code review, testing, and deployment with AI assistance throughout your development workflow.