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
styleprop or an unrestrictedclassNamemeans 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
innerClassNameorwrapperPropsfreezes 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.