Widget Component Library Design for ChatGPT Apps

Building ChatGPT applications at scale requires a robust, reusable component library. Unlike traditional web applications, ChatGPT widgets must adhere to strict OpenAI design constraints: system fonts only, maximum 2 CTAs per card, no nested scrolling, and WCAG AA accessibility compliance. A well-designed component library enforces these constraints while providing developers with flexible, composable primitives that accelerate development and ensure consistency.

Component libraries transform how teams build ChatGPT widgets. Instead of copying UI code between projects, developers import tested, documented components that comply with OpenAI's approval requirements. Design tokens provide a single source of truth for colors, typography, and spacing, ensuring visual consistency across applications. Compound component patterns enable complex interactions without violating OpenAI's two-CTA limit. Comprehensive documentation and automated testing prevent regressions and reduce onboarding time for new developers.

This guide demonstrates production-ready patterns for designing component libraries specifically for ChatGPT widgets. You'll learn how to structure design tokens that respect OpenAI's system font constraints, build flexible component APIs that prevent common anti-patterns, implement documentation workflows using Storybook, and establish versioning strategies that support multiple ChatGPT applications. Every code example reflects real-world production standards used by teams building at scale.

Whether you're a solo developer building your first ChatGPT app or an engineering team maintaining dozens of widgets, this guide provides actionable strategies for building component libraries that accelerate development, enforce compliance, and scale across applications. Let's explore how design tokens, component APIs, and testing strategies create the foundation for ChatGPT widget excellence.

Design Tokens: The Foundation of Consistency

Design tokens transform subjective design decisions into programmable constants. For ChatGPT widgets, tokens must reflect OpenAI's constraints: system fonts (SF Pro on iOS, Roboto on Android), WCAG AA contrast ratios, and spacing that works within inline card dimensions. Tokens should be platform-agnostic, expressed in formats that compile to CSS variables, React constants, and TypeScript types.

Start with a comprehensive token schema that covers colors, typography, spacing, shadows, and transitions. ChatGPT widgets require careful contrast management since inline cards appear against ChatGPT's light or dark themes. Define semantic color tokens (primary, secondary, error, success) alongside functional tokens (text-default, text-muted, border, background-elevated). OpenAI recommends 4.5:1 minimum contrast for text, so validate tokens against WCAG guidelines.

Typography tokens must reference system font stacks exactly as OpenAI specifies. Avoid custom fonts entirely—they violate approval requirements and increase bundle size. Define font families, weights, sizes, and line heights that work across iOS and Android. Scale tokens should support responsive text sizing without breaking layouts, crucial for accessibility compliance.

Spacing tokens create visual rhythm and ensure components align predictably. Use a base-8 or base-4 scale that divides evenly into ChatGPT's card dimensions. Shadow tokens provide depth cues while respecting mobile performance constraints—heavy shadows degrade perceived responsiveness. Transition tokens ensure animations feel native to ChatGPT's interface, avoiding jarring motion that conflicts with the chat experience.

Here's a production-ready design token configuration using Style Dictionary, which compiles to CSS variables, TypeScript constants, and React Native tokens:

// tokens/base.json (Design token source of truth)
{
  "color": {
    "brand": {
      "primary": { "value": "#0066CC", "type": "color" },
      "primary-hover": { "value": "#0052A3", "type": "color" },
      "primary-active": { "value": "#003D7A", "type": "color" }
    },
    "neutral": {
      "0": { "value": "#FFFFFF", "type": "color" },
      "50": { "value": "#F9FAFB", "type": "color" },
      "100": { "value": "#F3F4F6", "type": "color" },
      "200": { "value": "#E5E7EB", "type": "color" },
      "300": { "value": "#D1D5DB", "type": "color" },
      "400": { "value": "#9CA3AF", "type": "color" },
      "500": { "value": "#6B7280", "type": "color" },
      "600": { "value": "#4B5563", "type": "color" },
      "700": { "value": "#374151", "type": "color" },
      "800": { "value": "#1F2937", "type": "color" },
      "900": { "value": "#111827", "type": "color" }
    },
    "semantic": {
      "error": { "value": "#DC2626", "type": "color" },
      "warning": { "value": "#F59E0B", "type": "color" },
      "success": { "value": "#10B981", "type": "color" },
      "info": { "value": "#3B82F6", "type": "color" }
    }
  },
  "typography": {
    "font-family": {
      "system": {
        "value": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
        "type": "fontFamily"
      },
      "mono": {
        "value": "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace",
        "type": "fontFamily"
      }
    },
    "font-size": {
      "xs": { "value": "12px", "type": "dimension" },
      "sm": { "value": "14px", "type": "dimension" },
      "base": { "value": "16px", "type": "dimension" },
      "lg": { "value": "18px", "type": "dimension" },
      "xl": { "value": "20px", "type": "dimension" },
      "2xl": { "value": "24px", "type": "dimension" }
    },
    "font-weight": {
      "regular": { "value": "400", "type": "fontWeight" },
      "medium": { "value": "500", "type": "fontWeight" },
      "semibold": { "value": "600", "type": "fontWeight" },
      "bold": { "value": "700", "type": "fontWeight" }
    },
    "line-height": {
      "tight": { "value": "1.25", "type": "number" },
      "normal": { "value": "1.5", "type": "number" },
      "relaxed": { "value": "1.75", "type": "number" }
    }
  },
  "spacing": {
    "0": { "value": "0px", "type": "dimension" },
    "1": { "value": "4px", "type": "dimension" },
    "2": { "value": "8px", "type": "dimension" },
    "3": { "value": "12px", "type": "dimension" },
    "4": { "value": "16px", "type": "dimension" },
    "5": { "value": "20px", "type": "dimension" },
    "6": { "value": "24px", "type": "dimension" },
    "8": { "value": "32px", "type": "dimension" },
    "10": { "value": "40px", "type": "dimension" },
    "12": { "value": "48px", "type": "dimension" }
  },
  "shadow": {
    "sm": {
      "value": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
      "type": "shadow"
    },
    "base": {
      "value": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
      "type": "shadow"
    },
    "md": {
      "value": "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
      "type": "shadow"
    }
  },
  "transition": {
    "fast": { "value": "150ms", "type": "duration" },
    "base": { "value": "200ms", "type": "duration" },
    "slow": { "value": "300ms", "type": "duration" },
    "ease-out": { "value": "cubic-bezier(0, 0, 0.2, 1)", "type": "cubicBezier" }
  }
}

This token structure compiles to CSS variables, TypeScript constants, and platform-specific formats. The semantic color system ensures WCAG AA compliance while providing flexibility for light/dark themes. System font families exactly match OpenAI's requirements, preventing approval rejections. The base-4 spacing scale aligns with ChatGPT's card dimensions, ensuring consistent padding and margins across components.

Related: React Widget Components for ChatGPT Apps | Widget Theme System Implementation

Component API Design: Flexible, Safe, Composable

Component APIs determine how developers interact with your library. For ChatGPT widgets, APIs must prevent common anti-patterns: more than 2 CTAs per card, nested scrolling containers, custom fonts, and accessibility violations. Design APIs that make correct usage intuitive and incorrect usage difficult or impossible.

Start with a clear props interface that uses TypeScript for compile-time safety. Define explicit variants rather than allowing freeform styling—this prevents developers from accidentally introducing custom fonts or violating spacing constraints. For example, a Button component should accept variant="primary" | "secondary" rather than style={{ backgroundColor: '#custom' }}, which could bypass design tokens.

Composition patterns enable complex UIs within OpenAI's constraints. The compound component pattern (React Context + dot notation) allows related components to share state without prop drilling. For example, a Card component with Card.Header, Card.Body, and Card.Actions can enforce the two-CTA limit by throwing errors if more than two Card.Action children are rendered.

Render props and function-as-child patterns provide flexibility for dynamic content while maintaining type safety. A List component that accepts renderItem={(item) => <ListItem {...item} />} enables custom rendering without losing library-enforced accessibility attributes. Hooks extract stateful logic (pagination, filtering, sorting) into reusable functions that work across components.

Here's a production-ready Button component with variants, loading states, and accessibility compliance:

// components/Button/Button.tsx
import React, { forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Loader } from '../Loader';

const buttonVariants = cva(
  // Base styles (OpenAI compliance: system fonts, accessible touch targets)
  [
    'inline-flex items-center justify-center',
    'font-medium text-sm',
    'rounded-lg',
    'transition-colors duration-200',
    'focus:outline-none focus:ring-2 focus:ring-offset-2',
    'disabled:opacity-50 disabled:cursor-not-allowed',
    'min-h-[44px] px-4', // WCAG 2.1 touch target minimum
  ],
  {
    variants: {
      variant: {
        primary: [
          'bg-brand-primary text-white',
          'hover:bg-brand-primary-hover',
          'active:bg-brand-primary-active',
          'focus:ring-brand-primary',
        ],
        secondary: [
          'bg-transparent border-2 border-neutral-300',
          'text-neutral-700',
          'hover:bg-neutral-50',
          'active:bg-neutral-100',
          'focus:ring-neutral-300',
        ],
        ghost: [
          'bg-transparent',
          'text-neutral-700',
          'hover:bg-neutral-100',
          'active:bg-neutral-200',
          'focus:ring-neutral-300',
        ],
        destructive: [
          'bg-semantic-error text-white',
          'hover:bg-red-700',
          'active:bg-red-800',
          'focus:ring-semantic-error',
        ],
      },
      size: {
        sm: 'min-h-[36px] px-3 text-xs',
        md: 'min-h-[44px] px-4 text-sm',
        lg: 'min-h-[52px] px-6 text-base',
      },
      fullWidth: {
        true: 'w-full',
        false: 'w-auto',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
      fullWidth: false,
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  /** Display loading spinner and disable interaction */
  isLoading?: boolean;
  /** Accessible label for loading state */
  loadingText?: string;
  /** Icon to display before text */
  leftIcon?: React.ReactNode;
  /** Icon to display after text */
  rightIcon?: React.ReactNode;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant,
      size,
      fullWidth,
      isLoading = false,
      loadingText,
      leftIcon,
      rightIcon,
      children,
      disabled,
      className,
      ...props
    },
    ref
  ) => {
    const isDisabled = disabled || isLoading;

    return (
      <button
        ref={ref}
        disabled={isDisabled}
        className={buttonVariants({ variant, size, fullWidth, className })}
        aria-disabled={isDisabled}
        aria-busy={isLoading}
        {...props}
      >
        {isLoading && <Loader className="mr-2" size={size === 'sm' ? 14 : 16} />}
        {!isLoading && leftIcon && <span className="mr-2">{leftIcon}</span>}
        <span>{isLoading && loadingText ? loadingText : children}</span>
        {!isLoading && rightIcon && <span className="ml-2">{rightIcon}</span>}
      </button>
    );
  }
);

Button.displayName = 'Button';

This Button API prevents anti-patterns through design. Variants use design tokens exclusively—no custom colors. Size variants enforce WCAG 2.1's minimum 44px touch target. The isLoading prop manages loading states accessibly with aria-busy. TypeScript ensures compile-time safety, catching prop mismatches before deployment.

Here's a compound component pattern that enforces OpenAI's two-CTA limit:

// components/Card/Card.tsx
import React, { createContext, useContext, Children } from 'react';

interface CardContextValue {
  actionCount: number;
  registerAction: () => void;
}

const CardContext = createContext<CardContextValue | undefined>(undefined);

export function Card({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  const [actionCount, setActionCount] = React.useState(0);

  const registerAction = () => {
    setActionCount((prev) => {
      const newCount = prev + 1;
      if (newCount > 2) {
        throw new Error(
          'Card exceeded OpenAI maximum of 2 primary actions. Consider using secondary actions or links.'
        );
      }
      return newCount;
    });
  };

  return (
    <CardContext.Provider value={{ actionCount, registerAction }}>
      <div
        className="bg-white rounded-lg shadow-md overflow-hidden"
        role="article"
        {...props}
      >
        {children}
      </div>
    </CardContext.Provider>
  );
}

function CardHeader({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div className="px-6 py-4 border-b border-neutral-200" {...props}>
      {children}
    </div>
  );
}

function CardBody({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div className="px-6 py-4" {...props}>
      {children}
    </div>
  );
}

function CardActions({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  const context = useContext(CardContext);
  if (!context) {
    throw new Error('Card.Actions must be used within Card');
  }

  // Count primary actions on mount
  React.useEffect(() => {
    const primaryActions = Children.toArray(children).filter(
      (child) => React.isValidElement(child) && child.props.variant !== 'secondary'
    );
    primaryActions.forEach(() => context.registerAction());
  }, [children, context]);

  return (
    <div className="px-6 py-4 bg-neutral-50 flex gap-3" {...props}>
      {children}
    </div>
  );
}

Card.Header = CardHeader;
Card.Body = CardBody;
Card.Actions = CardActions;

This pattern enforces OpenAI compliance at the component level. Developers who exceed two CTAs receive immediate, actionable error messages. The Context API avoids prop drilling while maintaining type safety. Compound components feel natural to React developers while preventing approval-blocking mistakes.

Related: Complete Guide to Building ChatGPT Applications | Design System Best Practices for ChatGPT Apps

Documentation: Storybook for Interactive Examples

Component libraries without documentation create friction and inconsistency. Storybook provides an interactive workshop where developers explore components, experiment with props, and validate accessibility. For ChatGPT widget libraries, Storybook stories should demonstrate OpenAI compliance scenarios: two-CTA limits, system fonts, accessible color contrast, and mobile-responsive layouts.

Start with a comprehensive Storybook configuration that includes accessibility testing, responsive viewports, and design token documentation. Install essential addons: @storybook/addon-a11y for automated accessibility checks, @storybook/addon-viewport for mobile testing, and @storybook/addon-docs for auto-generated API documentation from TypeScript types.

Write stories that cover common use cases and edge cases. For a Button component, demonstrate all variants, sizes, loading states, disabled states, and icon combinations. Include a story that shows incorrect usage (more than two CTAs in a card) with error boundaries that display helpful messages. Canvas view lets developers interact with components; Docs view auto-generates prop tables from TypeScript interfaces.

Document OpenAI-specific constraints in MDX stories. Create a "ChatGPT Compliance" section for each component that lists relevant OpenAI requirements, explains how the component enforces them, and provides visual examples of compliant vs. non-compliant usage. Link to OpenAI's design guidelines and MakeAIHQ resources for deeper context.

Here's a production-ready Storybook configuration for ChatGPT widget components:

// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-a11y',
    '@storybook/addon-viewport',
    '@storybook/addon-interactions',
    '@storybook/addon-coverage',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
  staticDirs: ['../public'],
};

export default config;

// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/styles/tokens.css'; // Design tokens

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
    viewport: {
      viewports: {
        mobile: {
          name: 'Mobile (375px)',
          styles: { width: '375px', height: '667px' },
        },
        tablet: {
          name: 'Tablet (768px)',
          styles: { width: '768px', height: '1024px' },
        },
        desktop: {
          name: 'Desktop (1280px)',
          styles: { width: '1280px', height: '800px' },
        },
        chatgpt: {
          name: 'ChatGPT Inline Card',
          styles: { width: '380px', height: '600px' },
        },
      },
    },
    a11y: {
      config: {
        rules: [
          {
            id: 'color-contrast',
            enabled: true,
          },
          {
            id: 'label',
            enabled: true,
          },
        ],
      },
    },
  },
};

export default preview;

Here's a comprehensive Button story with accessibility testing and OpenAI compliance documentation:

// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { Card } from '../Card';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'ghost', 'destructive'],
      description: 'Visual style variant',
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
      description: 'Button size (affects padding and font size)',
    },
    isLoading: {
      control: 'boolean',
      description: 'Display loading spinner',
    },
  },
};

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

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Primary Action',
  },
};

export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-col gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
    </div>
  ),
};

export const WithIcons: Story = {
  render: () => (
    <div className="flex flex-col gap-4">
      <Button leftIcon={<span>→</span>}>With Left Icon</Button>
      <Button rightIcon={<span>←</span>}>With Right Icon</Button>
    </div>
  ),
};

export const LoadingStates: Story = {
  render: () => (
    <div className="flex flex-col gap-4">
      <Button isLoading>Loading...</Button>
      <Button isLoading loadingText="Submitting...">
        Submit
      </Button>
    </div>
  ),
};

export const ChatGPTCompliance: Story = {
  render: () => (
    <Card>
      <Card.Header>
        <h3>Flight Booking Confirmation</h3>
      </Card.Header>
      <Card.Body>
        <p>SFO → JFK, Jan 15, 2026</p>
      </Card.Body>
      <Card.Actions>
        <Button variant="primary">Confirm Booking</Button>
        <Button variant="secondary">View Details</Button>
      </Card.Actions>
    </Card>
  ),
  parameters: {
    docs: {
      description: {
        story:
          'Demonstrates OpenAI compliance: maximum 2 CTAs, system fonts, 44px touch targets, WCAG AA contrast.',
      },
    },
  },
};

This Storybook setup enables rapid development and confident deployments. The accessibility addon catches contrast violations before code review. Custom viewports test mobile responsiveness and ChatGPT inline card dimensions. Auto-generated documentation from TypeScript eliminates documentation drift.

Related: React Widget Components for ChatGPT Apps

Versioning and Publishing: Scalable Distribution

Component libraries evolve continuously—bug fixes, new components, breaking API changes. Semantic versioning communicates change impact to consumers: patch versions (1.0.1) for bug fixes, minor versions (1.1.0) for backward-compatible features, major versions (2.0.0) for breaking changes. Automated changelogs transform commit messages into human-readable release notes.

Use Changesets or semantic-release to automate versioning workflows. Developers create changeset files describing their changes (patch, minor, or major). On merge to main, CI generates version bumps, updates CHANGELOG.md, and publishes to npm. This workflow ensures consistent versioning and eliminates manual errors.

Monorepo setups support multiple related packages: @your-lib/components, @your-lib/tokens, @your-lib/hooks. Turborepo or Nx provide caching and parallel execution, dramatically reducing CI times. Independent versioning allows tokens to evolve independently from components, reducing unnecessary updates for consumers.

Publishing to npm requires careful configuration. Set main to CommonJS, module to ESM, and types to TypeScript declarations. Include source maps for debugging. Tree-shaking support via sideEffects: false reduces bundle sizes for consumers. Provenance signing (npm's --provenance flag) proves package authenticity.

Here's a production-ready npm publishing configuration and automated release script:

// package.json (Component library configuration)
{
  "name": "@yourcompany/chatgpt-ui",
  "version": "1.2.3",
  "description": "OpenAI-compliant component library for ChatGPT widgets",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.cjs",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    },
    "./tokens": {
      "require": "./dist/tokens.cjs",
      "import": "./dist/tokens.mjs",
      "types": "./dist/tokens.d.ts"
    }
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "sideEffects": false,
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts --sourcemap",
    "build:tokens": "style-dictionary build",
    "prepublishOnly": "npm run build && npm run build:tokens",
    "release": "changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.1",
    "tsup": "^8.0.1",
    "style-dictionary": "^3.9.0"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/",
    "provenance": true
  }
}

Automated release workflow using GitHub Actions:

# .github/workflows/release.yml
name: Release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Create Release PR or Publish
        uses: changesets/action@v1
        with:
          publish: npm run release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

This setup automates the entire release process. Developers create changesets describing their changes. On merge, GitHub Actions builds the library, bumps versions, updates CHANGELOG.md, publishes to npm, and creates GitHub releases. Provenance signing proves packages originated from your repository, enhancing supply chain security.

Related: Widget Theme System Implementation

Testing Strategies: Confidence Through Automation

Component libraries require comprehensive testing to prevent regressions. Unit tests validate component logic and props interfaces. Visual regression tests catch unintended styling changes. Accessibility tests ensure WCAG compliance. Integration tests verify compound components interact correctly. ChatGPT widget libraries must also test OpenAI-specific constraints: two-CTA limits, system font rendering, touch target sizes.

Use Vitest or Jest for unit testing. Test component rendering, prop variants, event handlers, and error boundaries. For the Card component, verify it throws errors when more than two primary CTAs are rendered. For Button, confirm loading states disable interaction and display accessible loading indicators. Mock React Context to test compound components in isolation.

Visual regression testing with Playwright or Chromatic catches styling regressions invisible to unit tests. Capture screenshots of components in various states (default, hover, focus, disabled), then compare against baseline images. ChatGPT widget tests should include mobile viewports and both light/dark themes to ensure contrast ratios remain compliant.

Accessibility testing with axe-core or Testing Library's @testing-library/jest-dom catches WCAG violations. Test keyboard navigation (Tab, Enter, Escape), screen reader labels (aria-label, aria-describedby), color contrast, and focus management. For ChatGPT widgets, verify touch targets meet the 44px minimum and that interactive elements have accessible names.

Here's a comprehensive test suite for the Button component:

// components/Button/Button.test.tsx
import { render, screen } 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', () => {
  it('renders children correctly', () => {
    render(<Button>Click Me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

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

    rerender(<Button variant="secondary">Secondary</Button>);
    expect(button).toHaveClass('border-neutral-300');
  });

  it('handles loading state', () => {
    render(<Button isLoading loadingText="Loading...">Submit</Button>);
    const button = screen.getByRole('button');
    expect(button).toHaveAttribute('aria-busy', 'true');
    expect(button).toBeDisabled();
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('calls onClick handler', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    await userEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('prevents click when disabled', async () => {
    const handleClick = vi.fn();
    render(<Button disabled onClick={handleClick}>Disabled</Button>);
    await userEvent.click(screen.getByRole('button'));
    expect(handleClick).not.toHaveBeenCalled();
  });

  it('meets minimum touch target size', () => {
    render(<Button size="md">Touch Target</Button>);
    const button = screen.getByRole('button');
    const styles = window.getComputedStyle(button);
    const minHeight = parseInt(styles.minHeight);
    expect(minHeight).toBeGreaterThanOrEqual(44);
  });

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

  it('supports keyboard navigation', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Keyboard Nav</Button>);
    const button = screen.getByRole('button');
    button.focus();
    expect(button).toHaveFocus();
    await userEvent.keyboard('{Enter}');
    expect(handleClick).toHaveBeenCalled();
  });
});

Visual regression test using Playwright:

// components/Button/Button.visual.test.ts
import { test, expect } from '@playwright/test';

test.describe('Button Visual Regression', () => {
  test('renders all variants correctly', async ({ page }) => {
    await page.goto('/storybook/?path=/story/components-button--all-variants');
    await page.waitForSelector('button');
    await expect(page).toHaveScreenshot('button-variants.png');
  });

  test('hover state appears correctly', async ({ page }) => {
    await page.goto('/storybook/?path=/story/components-button--primary');
    const button = page.getByRole('button');
    await button.hover();
    await expect(page).toHaveScreenshot('button-hover.png');
  });

  test('focus state shows ring', async ({ page }) => {
    await page.goto('/storybook/?path=/story/components-button--primary');
    const button = page.getByRole('button');
    await button.focus();
    await expect(page).toHaveScreenshot('button-focus.png');
  });

  test('loading state displays spinner', async ({ page }) => {
    await page.goto('/storybook/?path=/story/components-button--loading-states');
    await expect(page).toHaveScreenshot('button-loading.png');
  });
});

This testing strategy catches bugs before production. Unit tests verify component logic and OpenAI compliance constraints. Visual regression tests detect unintended styling changes. Accessibility tests ensure WCAG compliance. Together, they provide confidence that components work correctly across ChatGPT's diverse runtime environments.

Related: Design System Best Practices for ChatGPT Apps

Conclusion: Build Scalable Component Libraries

Component libraries transform ChatGPT widget development from error-prone repetition into systematic composition. Design tokens enforce OpenAI compliance while providing flexibility for brand expression. Well-designed component APIs prevent anti-patterns through TypeScript safety and compound component constraints. Storybook documentation accelerates onboarding and reduces support burden. Automated versioning and testing ensure reliable releases.

The patterns demonstrated here reflect production standards used by teams building ChatGPT applications at scale. Design tokens compiled with Style Dictionary work across platforms. Component APIs using class-variance-authority provide type-safe variant systems. Storybook with accessibility addons catches violations before deployment. Changesets automate semantic versioning. Comprehensive testing prevents regressions.

Whether you're building a single ChatGPT app or a platform generating thousands of widgets, these component library strategies reduce development time, improve quality, and ensure OpenAI approval on first submission. Invest in your component library—it compounds returns across every project.

Ready to build ChatGPT applications faster with pre-built, OpenAI-compliant components? MakeAIHQ provides a complete component library, visual editor, and deployment pipeline specifically designed for ChatGPT widgets. From zero to ChatGPT App Store in 48 hours—no coding required.

Frequently Asked Questions

How do design tokens prevent OpenAI approval rejections?

Design tokens enforce OpenAI constraints at the source level. By defining system font families, WCAG-compliant colors, and proper spacing scales as tokens, you prevent developers from introducing custom fonts or insufficient contrast ratios. When compiled to CSS variables and TypeScript constants, tokens become the single source of truth that components reference, ensuring every UI element complies with OpenAI requirements.

What's the benefit of compound components over prop-based APIs?

Compound components using React Context provide better ergonomics for complex UI patterns while enforcing constraints. For example, a Card component with Card.Actions can count primary CTAs and throw errors if the limit exceeds two. This is cleaner than passing actions={[...]} props and manually validating array length. Compound components also support better composition and feel more natural to React developers.

How does Storybook improve component library adoption?

Storybook provides interactive documentation that developers actually use. Instead of reading API docs and guessing prop combinations, developers see live components, experiment with props, and copy working code. For ChatGPT widget libraries, Storybook stories demonstrate OpenAI compliance scenarios visually, reducing approval rejections. The accessibility addon catches WCAG violations during development, not after deployment.

Should I publish multiple npm packages or a monorepo?

Monorepos support independent versioning of related packages. Publish @yourlib/components, @yourlib/tokens, and @yourlib/hooks separately so consumers only install what they need. Design tokens can evolve independently from components, reducing unnecessary updates. Tools like Turborepo provide caching and parallel execution, making CI faster. For small libraries, a single package may suffice, but monorepos scale better as complexity grows.

How do I ensure backward compatibility when updating components?

Use semantic versioning strictly: patch for bug fixes, minor for backward-compatible features, major for breaking changes. Changesets automate this by requiring developers to declare change types. Deprecate features before removing them—add console warnings in minor versions, remove in major versions. Comprehensive testing (unit, visual regression, accessibility) catches unintended breaking changes. Publish release candidates (1.0.0-rc.1) for major versions to gather feedback before final release.


Schema Markup (HowTo):

{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "How to Design Component Libraries for ChatGPT Widgets",
  "description": "Build reusable component libraries for ChatGPT widgets. Design tokens, component API, documentation, versioning with React production examples.",
  "step": [
    {
      "@type": "HowToStep",
      "name": "Design Tokens",
      "text": "Create platform-agnostic design tokens for colors, typography, spacing, shadows, and transitions that enforce OpenAI constraints like system fonts and WCAG contrast ratios."
    },
    {
      "@type": "HowToStep",
      "name": "Component API Design",
      "text": "Build flexible, type-safe component APIs using TypeScript, variants, and compound components that prevent anti-patterns like exceeding two CTAs per card."
    },
    {
      "@type": "HowToStep",
      "name": "Documentation",
      "text": "Set up Storybook with accessibility testing, responsive viewports, and interactive examples demonstrating OpenAI compliance scenarios."
    },
    {
      "@type": "HowToStep",
      "name": "Versioning & Publishing",
      "text": "Implement automated semantic versioning with Changesets, publish to npm with provenance signing, and maintain monorepo architecture for scalability."
    },
    {
      "@type": "HowToStep",
      "name": "Testing Strategies",
      "text": "Write comprehensive unit tests, visual regression tests, and accessibility tests to ensure components meet WCAG and OpenAI requirements."
    }
  ]
}