Component Architecture
Component API design, composition patterns, and architecture for scalable design system components
You are an expert in component API design and composition for building and maintaining design systems. ## Key Points - **Minimal API** — expose only what consumers need; every prop is a maintenance commitment. - **Consistent naming** — use the same prop names across components (`size`, `variant`, `disabled`). - **Typed contracts** — use TypeScript or PropTypes so consumers get autocomplete and compile-time safety. - **Sensible defaults** — components should render correctly with zero props where possible. 1. **Primitives** — Box, Text, Stack, Icon (low-level layout building blocks) 2. **Core components** — Button, Input, Select, Modal, Tooltip 3. **Composite patterns** — DataTable, FormField, AppShell, NavigationMenu - Spread remaining props onto the root DOM element so consumers can add `className`, `data-*` attributes, and event handlers without wrapper hacks. - Enforce variant constraints with TypeScript unions rather than accepting arbitrary strings, so invalid states are caught at compile time. - Separate visual (styling) concerns from behavioral (state/logic) concerns — this enables headless extraction and cross-framework reuse. - Accepting a `children` render function and a declarative prop for the same slot, which creates ambiguous behavior and confusing docs. - Over-abstracting early — shipping a flexible but hard-to-use API is worse than shipping a simple component and extending it when real use cases emerge.
skilldb get design-systems-skills/Component ArchitectureFull skill: 172 linesComponent Architecture — Design Systems
You are an expert in component API design and composition for building and maintaining design systems.
Overview
Component architecture defines how design system components expose their API, compose together, and scale across a product ecosystem. Good architecture balances flexibility for consumers with consistency enforcement, making the right thing easy and the wrong thing hard.
Core Philosophy
Component architecture is the art of designing APIs that make the right thing easy and the wrong thing hard. Every prop you expose is a maintenance commitment — it must be documented, tested across all combinations, and supported through future versions. The best component APIs are small, consistent, and unsurprising: a developer who has used your Button should be able to guess the API of your Badge without reading the docs.
Composition is the primary scaling mechanism for design system components. Rather than building monolithic components with dozens of props to cover every use case, build small focused pieces that compose together. A FormField that wraps a Label, an Input, and an ErrorMessage is more flexible than an Input component with label, error, helperText, prefix, and suffix props. Compound components and slot patterns give consumers control over structure while the system retains control over behavior and accessibility.
The tension between flexibility and consistency is the central design challenge. Too rigid and product teams work around the system; too flexible and the system provides no value. The resolution is to be opinionated about behavior, accessibility, and interaction patterns while being flexible about visual presentation. A Dialog component should always trap focus and handle escape-key dismissal, but it should accept any content in its body.
Core Concepts
API Surface Principles
- Minimal API — expose only what consumers need; every prop is a maintenance commitment.
- Consistent naming — use the same prop names across components (
size,variant,disabled). - Typed contracts — use TypeScript or PropTypes so consumers get autocomplete and compile-time safety.
- Sensible defaults — components should render correctly with zero props where possible.
Composition Models
| Model | Description | Example |
|---|---|---|
| Configuration | Single component with props controlling output | <Button variant="primary" /> |
| Compound | Parent + children sharing implicit state | <Tabs><Tab /><TabPanel /></Tabs> |
| Slot-based | Named slots/render props for custom regions | <Card header={<Heading />} /> |
| Headless | Logic-only hooks, consumer provides markup | useCombobox() + custom JSX |
Component Tiers
- Primitives — Box, Text, Stack, Icon (low-level layout building blocks)
- Core components — Button, Input, Select, Modal, Tooltip
- Composite patterns — DataTable, FormField, AppShell, NavigationMenu
Implementation Patterns
Compound Component with Context
import { createContext, useContext, useState, ReactNode } from 'react';
interface AccordionContextValue {
openItem: string | null;
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const ctx = useContext(AccordionContext);
if (!ctx) throw new Error('Accordion children must be used within <Accordion>');
return ctx;
}
export function Accordion({ children }: { children: ReactNode }) {
const [openItem, setOpenItem] = useState<string | null>(null);
const toggle = (id: string) => setOpenItem(prev => (prev === id ? null : id));
return (
<AccordionContext.Provider value={{ openItem, toggle }}>
<div role="region">{children}</div>
</AccordionContext.Provider>
);
}
export function AccordionItem({ id, title, children }: {
id: string;
title: string;
children: ReactNode;
}) {
const { openItem, toggle } = useAccordion();
const isOpen = openItem === id;
return (
<div>
<button
aria-expanded={isOpen}
aria-controls={`panel-${id}`}
onClick={() => toggle(id)}
>
{title}
</button>
{isOpen && <div id={`panel-${id}`} role="region">{children}</div>}
</div>
);
}
Polymorphic as Prop
import { ElementType, ComponentPropsWithoutRef } from 'react';
type ButtonProps<T extends ElementType = 'button'> = {
as?: T;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
} & ComponentPropsWithoutRef<T>;
export function Button<T extends ElementType = 'button'>({
as,
variant = 'primary',
size = 'md',
...props
}: ButtonProps<T>) {
const Component = as ?? 'button';
return <Component data-variant={variant} data-size={size} {...props} />;
}
// Usage
<Button as="a" href="/home" variant="ghost">Home</Button>
Forwarding Refs and Spreading Props
import { forwardRef, ComponentPropsWithRef } from 'react';
interface InputProps extends ComponentPropsWithRef<'input'> {
label: string;
error?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, id, ...rest }, ref) => {
const inputId = id ?? `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input ref={ref} id={inputId} aria-invalid={!!error} {...rest} />
{error && <span role="alert">{error}</span>}
</div>
);
}
);
Best Practices
- Spread remaining props onto the root DOM element so consumers can add
className,data-*attributes, and event handlers without wrapper hacks. - Enforce variant constraints with TypeScript unions rather than accepting arbitrary strings, so invalid states are caught at compile time.
- Separate visual (styling) concerns from behavioral (state/logic) concerns — this enables headless extraction and cross-framework reuse.
Common Pitfalls
- Accepting a
childrenrender function and a declarative prop for the same slot, which creates ambiguous behavior and confusing docs. - Over-abstracting early — shipping a flexible but hard-to-use API is worse than shipping a simple component and extending it when real use cases emerge.
Anti-Patterns
-
The god component. A single component with 30+ props that handles every possible variation through conditional rendering. This produces unmaintainable code, untestable state combinations, and an API that intimidates consumers. Break it into composable pieces.
-
Prop drilling through wrapper layers. Wrapping a native element in multiple abstraction layers where each layer passes props through to the next without adding value. Consumers end up writing
<Input inputProps={{ style: {...} }}instead of spreading props directly onto the root element. -
Inconsistent prop naming across components. Using
typeon Button,varianton Badge,kindon Alert, andstyleon Card for the same conceptual idea (visual variant) forces consumers to memorize arbitrary differences. Standardize on one name. -
Boolean prop explosion. Props like
isPrimary,isSecondary,isGhost,isOutlinedare mutually exclusive states disguised as independent flags. Use a singlevariantunion type instead, which makes invalid states unrepresentable. -
Swallowing native HTML attributes. Components that do not spread remaining props onto the root DOM element force consumers into workarounds for standard attributes like
className,data-testid,aria-describedby, and event handlers. Always forward unknown props to the underlying element.
Install this skill directly: skilldb add design-systems-skills
Related Skills
Design System Governance
Versioning, contribution processes, and governance models for design system teams
Design Tokens
Design tokens for colors, spacing, typography, and other visual primitives in design systems
Icon Systems
Icon systems, SVG management, and scalable icon delivery pipelines for design systems
Motion Guidelines
Motion and animation guidelines, easing curves, and transition patterns for design systems
Responsive Patterns
Responsive design patterns, fluid layouts, and adaptive component strategies for design systems
Storybook Docs
Storybook documentation, visual testing, and interactive component cataloging for design systems