Tags: web-dev concept

Component API Design

Date: 2026-08-16


A component’s props are a public interface with consumers you don’t control. Everything you expose becomes something you can’t change — so the design question is what to leave out.


Component API design is deciding what a component accepts, what it names those things, and what it deliberately refuses to expose.

The framing that makes it tractable: props are an API contract, not configuration. Every prop is a promise to keep it working, and removing one is a breaking change for every consumer — Design System Versioning, Backwards Compatibility.

Design the smallest thing that works

<Button>Save</Button>

That should be valid. A component requiring six props to render is a component that has pushed its decisions onto its consumer — and a design system exists to make those decisions once.

Sensible defaults for everything, and the common case needs nothing:

<Button>Save</Button>
<Button variant="danger">Delete</Button>
<Button variant="danger" size="sm" loading>
  Delete
</Button>

Each rung adds one thing. Nobody has to learn the third form to use the first.

Name by intent

AVOID                 PREFER
isBlue                variant="primary"
hasIcon               (infer from children)
size={2}              size="sm"
showBorder            variant="outline"
isSmall + isTiny      size="sm" | "xs"

Two booleans that can’t both be true should be one enumerated prop. isSmall and isTiny permit an invalid state; size doesn’t — and a type system can then enforce it — Type Systems.

Name by what the consumer wants, not by what you implement. variant="danger" is intent. backgroundColor="red" is implementation, and it fixes the colour forever — Colour Systems.

What not to expose

The discipline that keeps a system a system.

  • No arbitrary style overrides. A style prop or an unrestricted className means any consumer can break out of the system, and they will — usually to solve a spacing problem the component should have handled
  • No external margin. A component setting its own outside spacing can’t be reused, because that’s a decision belonging to the layout — Spacing Systems
  • No internal structure. Exposing innerClassName or wrapperProps freezes your DOM as public API. You can never refactor it
  • No one-off props. A prop added for one consumer’s edge case is a permanent obligation. Look for the general case, or say no

The test: would I be happy to still support this in three years? If not, don’t ship it.

Escape hatches, done properly

Refusing everything makes a system people work around. The answer is bounded escape hatches:

GOOD                       BAD
slots for content          style={{...}}
asChild / polymorphic      className passthrough
                             with no constraint
documented CSS custom      !important in
  properties on the          consumer code
  component

A CSS custom property the component reads is an escape hatch you control — you can rename the internals freely, and the consumer’s override still works — Design Tokens.

Controlled and uncontrolled

Interactive components need both, and the choice belongs to the consumer:

UNCONTROLLED   the component owns state
  <Select defaultValue="a" />

CONTROLLED     the consumer owns state
  <Select value={v} onChange={setV} />

Support both, and never both at once. The bug this creates — a value prop with no onChange, so the field appears frozen — is one of the most common in component libraries. Warn on it in development.

Consistency across the set

The single highest-value property of a component API, and the easiest to lose.

every component uses the same
  size      "xs" | "sm" | "md" | "lg"
  variant   naming convention
  onChange  signature shape
  disabled  behaviour

If Button takes size="sm" and Input takes small, learning one component teaches you nothing about the next. A system’s real value is that the second component is free to learn — inconsistency spends exactly that.

Write the conventions down before the third component, because retrofitting them is a breaking change across the library — Component Documentation.