JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionssystem-design
PrevNext

Learn the concept

Component Architecture at Scale

system-design
junior
component-architecture

What makes a good component API for a design system?

design-system
component-api
composition
compound-components
accessibility
typescript
Quick Answer

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.

Detailed Explanation

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.

Composition Over Configuration

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:

JSX
<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:

JSX
<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

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:

JSX
<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.

Sensible Defaults

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:

JSX
// 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.

Consistent Naming Conventions

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.

Accessibility by Default

Components should be accessible without the consumer doing extra work:

  • Buttons render as <button> elements (not <div>) with built-in keyboard support
  • Form fields associate labels with inputs via htmlFor/id
  • Dialogs trap focus, handle Escape key, and manage aria-modal
  • Dropdowns implement full keyboard navigation (Arrow keys, Enter, Escape, Home/End)

If 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:

TSX
type IconButtonProps = {
  'aria-label': string; // Required — no icon button without a label
  icon: React.ReactNode;
  size?: 'sm' | 'md' | 'lg';
};

Type Safety with TypeScript

Use union types to constrain valid values. This provides autocompletion and compile-time error checking:

TSX
// 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;
};

Key Interview Distinction

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.

Code Examples

Bad vs good component API designTSX
// 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>

Real-World Applications

Use Cases

Design System Development

Building a shared component library consumed by multiple product teams — API design decisions affect developer experience and consistency across the entire organization

Open Source Component Libraries

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

White-Label Products

Creating customizable component sets for white-label products where clients apply their own branding — headless components with styling slots enable full visual customization

Mini Projects

Mini Design System

intermediate

Build 5 compound components (Button, Dialog, Select, Accordion, Tooltip) with consistent naming, TypeScript props, and keyboard accessibility — document each in Storybook

API Review Checklist

beginner

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

Industry Examples

Radix UI

Pioneered the compound component + asChild pattern for unstyled primitives — Dialog, Select, DropdownMenu, and 28+ components that provide behavior and accessibility with zero styling opinions

Shopify

Polaris design system provides compound components with consistent API conventions (size, variant, disabled) across 60+ components, all documented with usage guidelines and accessibility notes

Adobe

React Aria provides hooks-based headless component primitives focused on accessibility — each hook handles ARIA attributes, keyboard navigation, and focus management for a specific pattern

Resources

Radix UI — Primitives

docs

Headless UI

docs

Atomic Design by Brad Frost

article
Previous
What are the trade-offs between client-side and server-side rendering?
Next
What is the difference between client state and server state?
PrevNext