Learn the concept
Component Architecture at Scale
A good component API follows composition over configuration (prefer children and slots over complex props), provides sensible defaults so components work with minimal setup, uses consistent naming conventions, ensures accessibility by default, and leverages TypeScript for type-safe constrained values.
A component API is the public interface that consumers use to configure and compose a component. In a design system used by dozens of developers across multiple teams, API design decisions compound — a confusing API creates bugs and inconsistency everywhere it's used, while a well-designed API makes the right thing easy and the wrong thing hard.
The most important principle in component API design is composition over configuration: prefer composable building blocks over monolithic components with many props.
Configuration approach (anti-pattern): A single component with dozens of props to handle every variation:
<Button
icon="save"
iconPosition="left"
iconSize="sm"
loading={true}
loadingText="Saving..."
tooltip="Save your work"
tooltipPosition="top"
/>This approach leads to prop explosion — every new feature adds more props, the component becomes harder to understand, and edge cases multiply. What if you want two icons? A custom loading spinner? An icon that's a React component, not a string?
Composition approach (preferred): Smaller, composable pieces that consumers assemble:
<Tooltip content="Save your work">
<Button disabled={isSaving}>
{isSaving ? <Spinner size="sm" /> : <SaveIcon />}
{isSaving ? 'Saving...' : 'Save'}
</Button>
</Tooltip>Composition gives consumers full control over structure, order, and content. It handles edge cases naturally because consumers compose the pieces they need.
Compound components take composition further by sharing implicit state between a parent and its children. The consumer controls the structure; the parent manages the behavior:
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Settings</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Description>Configure your preferences.</Dialog.Description>
{/* Consumer adds any content here */}
<form>{/* ... */}</form>
<Dialog.Close asChild>
<Button variant="ghost">Cancel</Button>
</Dialog.Close>
</Dialog.Content>
</Dialog>The Dialog parent manages open/closed state, focus trapping, and escape key handling. The consumer decides what the trigger looks like, what content appears, and where the close button goes. This pattern is used by Radix UI, Headless UI, and Reach UI.
Components should work correctly with minimal props. A <Button> should look like a button, have proper focus styles, and be accessible without the consumer specifying anything beyond children:
// Works out of the box — renders a styled, accessible button
<Button>Save</Button>
// Optional props for customization
<Button variant="destructive" size="lg" disabled>Delete Account</Button>Defaults should represent the most common use case. If 80% of buttons are medium-sized primary buttons, that should be the default.
A design system's API should feel like a single cohesive product:
size — not sz, dimension, or scale. Values: 'sm' | 'md' | 'lg' (not 'small' | 'medium' | 'large')variant — not type, kind, or appearance. Values: 'default' | 'destructive' | 'outline' | 'ghost'disabled — not isDisabled. Follow native HTML attribute naming.onValueChange — for controlled value changes. Follows the on[Event] convention.defaultValue — for uncontrolled initial value. Matches React's convention.asChild — for polymorphic rendering (Radix pattern). Merges behavior onto the child element.Components should be accessible without the consumer doing extra work:
<button> elements (not <div>) with built-in keyboard supporthtmlFor/idaria-modalIf a component requires the consumer to add accessibility manually (e.g., aria-label for an icon-only button), document it clearly and enforce it with TypeScript:
type IconButtonProps = {
'aria-label': string; // Required — no icon button without a label
icon: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
};Use union types to constrain valid values. This provides autocompletion and compile-time error checking:
// Good — constrained, autocomplete-friendly
type ButtonProps = {
variant?: 'default' | 'destructive' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
};
// Bad — any string accepted, no autocomplete, runtime errors
type ButtonProps = {
variant?: string;
size?: string;
};Good component APIs follow composition over configuration (composable children beat complex props), provide sensible defaults (work with minimal setup), use consistent naming (feel like one product), ensure accessibility by default (correct HTML elements, keyboard support, ARIA), and leverage TypeScript for type safety. The compound component pattern (Dialog, Select, Accordion) is the gold standard for complex interactive components — it separates behavior from structure.
// BAD: Configuration-heavy API (prop explosion)
// What if you need two icons? A custom spinner? An image instead of icon?
<Card
title="Product Name"
subtitle="$29.99"
description="A great product for your needs"
image="/product.jpg"
imagePosition="top"
imageAlt="Product photo"
badge="New"
badgeColor="green"
actions={[{ label: 'Buy', onClick: handleBuy }]}
actionsPosition="bottom"
footer="Free shipping"
/>
// GOOD: Composition-based API (flexible, readable)
<Card>
<Card.Image src="/product.jpg" alt="Product photo" />
<Card.Header>
<Badge variant="success">New</Badge>
<Card.Title>Product Name</Card.Title>
<Card.Description>A great product for your needs</Card.Description>
</Card.Header>
<Card.Content>
<p className="text-2xl font-bold">$29.99</p>
</Card.Content>
<Card.Footer>
<span>Free shipping</span>
<Button onClick={handleBuy}>Buy Now</Button>
</Card.Footer>
</Card>Building a shared component library consumed by multiple product teams — API design decisions affect developer experience and consistency across the entire organization
Publishing reusable components (like Radix, Headless UI, or Shadcn/ui) where the API must be intuitive for thousands of developers who read docs once and use from memory
Creating customizable component sets for white-label products where clients apply their own branding — headless components with styling slots enable full visual customization
Build 5 compound components (Button, Dialog, Select, Accordion, Tooltip) with consistent naming, TypeScript props, and keyboard accessibility — document each in Storybook
Create a checklist tool that evaluates a component's API against best practices (composition over config, sensible defaults, consistent naming, a11y, type safety) and gives a score
Pioneered the compound component + asChild pattern for unstyled primitives — Dialog, Select, DropdownMenu, and 28+ components that provide behavior and accessibility with zero styling opinions
Polaris design system provides compound components with consistent API conventions (size, variant, disabled) across 60+ components, all documented with usage guidelines and accessibility notes