The Journal

Design Systems3 min read

Component API Design: How to Build Components People Love Using

The difference between a component library people love and one they avoid is the API. Here's how to design component interfaces that just make sense.

The best components disappear — you reach for one, it does the obvious thing, and you move on without opening the docs. The worst make you stop and think: twenty props, half of them required, none named the way you’d guess. The gap between them is API design, and it comes down to a few conventions applied without exception.

Compose, don’t configure

The instinct is to add a prop for every variation. A card sprouts headerTitle, headerSubtitle, showFooter, footerAlign, and collapses under its own options. Every new design becomes another boolean, until the type signature is a wall nobody reads.

Composition is the way out: expose the parts. Ship Card, CardHeader, CardContent, and CardFooter and let people arrange them. You write less, and the layout they need but you never imagined is just JSX.

Name variants the same way

Pick a vocabulary and use it in every component. variant for visual style — default, ghost, destructive. size for scale — sm, default, lg. Two names, learned once, applied to forty components.

Consistency is the whole game here. When Button, Badge, and Alert all take the same variant names, people guess right the first time and stop reading prop tables.

Leave escape hatches

You won’t predict every use, so build the exits in from the start. Two cover almost everything:

  • className, merged with the component’s own classes via tailwind-merge, so a caller can adjust one edge case without forking.
  • asChild, which renders your component as its child element through Radix Slot — so a Button can become a Next.js Link without duplicating a single style.

This is why copy-paste libraries feel so flexible. Spectrum UI wires className and asChild in by default, so the hatch is there before you reach for it. You inherit the flexibility without having to design for it.

The rule

Design the common case to need zero props, and make the rare case possible without a fork. If someone can render your component with <Button>Save</Button> and also bend it to a one-off without editing your source, the API is right.

Ship that, and people stop rebuilding what you already gave them — which is the only metric a component API really has. The best compliment yours gets is silence: no issues, no forks, no Slack thread asking how it works.

Frequently asked questions

What makes a good component API?
A good component API favors composition over configuration: instead of piling props like headerTitle and showFooter onto one component, expose parts like Card, CardHeader, CardContent, and CardFooter and let people arrange them. This keeps type signatures small and lets callers build layouts you never imagined with plain JSX.
How do you keep component props consistent across a library?
Pick one vocabulary and use it everywhere: variant for visual style (default, ghost, destructive) and size for scale (sm, default, lg). When Button, Badge, and Alert all take the same variant names, people guess right the first time and stop reading prop tables. Two names, learned once, applied across every component.
What are escape hatches in component design?
Escape hatches let callers handle cases you didn't predict without forking your source. The two that cover almost everything are a className prop merged via tailwind-merge, so a caller can adjust one edge case, and asChild, which renders your component as its child through Radix Slot, so a Button can become a Next.js Link.