Bad component APIs don’t announce themselves as bad on the day they’re created. They announce themselves six months later, when your team is working around them in creative ways, and the workarounds have become load-bearing. Designing component APIs well upfront is one of those investments that pays out quietly and continuously.
The Hidden Cost of a Bad Component API
Most component libraries start with good intentions and end up with a graveyard of isSpecialCase props, override escape hatches, and duplicate components that exist because the original couldn’t flex enough. The problem is rarely carelessness. It’s that component APIs are usually designed by looking at what the component does today, not how it will be called by people who didn’t build it.
A component API is a contract. When it’s vague or inconsistent, consuming code gets messy. When it’s too rigid, people stop using the component and write their own version. Either failure costs the team time and consistency.
The best component API is the one your team reaches for first, not the one they rewrite after three uses.
Start With How It Will Be Called
The most common mistake is designing a component’s internals first and exposing props as an afterthought. The better approach: write the call site before writing the component.
Sketch out three or four realistic usage examples in pseudocode. A primary button. A destructive confirmation button. A button in a loading state. A button with a leading icon. If writing those four call sites feels awkward, the API has a problem before a line of implementation code exists.
This technique surfaces issues early:
- Props that are too low-level (forcing consumers to know about internal state)
- Props that are too bundled (one prop doing two separate jobs)
- Missing props that every real use case would need
Call-site-first design also helps you see whether the API reads like plain language or like configuration. <Button variant="destructive"> reads clearly. <Button type={2} danger={true} confirm> does not.
Naming: Be Boring on Purpose
Component prop naming is not the place for creativity. Predictable, boring names reduce the time a developer spends in documentation. They also reduce bugs from guessing.
A few rules that pay off consistently:
Match the HTML attribute where there’s one. If the native element has disabled, use disabled, not isDisabled or inactive. Teams that mix conventions end up with both.
Use the is prefix only for boolean state, not for variants. isLoading and isDisabled are reasonable. isPrimary as an alternative to variant="primary" is not. One describes transient state; the other chooses between named design options.
Pick a convention for object-shaped props and hold it. If your icon slot accepts a React component, decide once and document it. icon={<SearchIcon />} or icon={SearchIcon} (the class) are both defensible, but mixing them across a library creates silent inconsistency.
Naming is also where design tokens connect to components. A token like color.feedback.danger should map to something obvious at the component level. When the naming between token layer and component layer drifts, the whole system becomes harder to reason about.
Variants vs. Composition
The variant question comes up in every design system: should you add a new variant, or should you compose smaller components?
Variants are right when the differences are visual but the behaviour is identical. A filled button and an outlined button do the same thing, triggered the same way. The difference is style. Variants handle that cleanly.
Composition is right when the differences are structural or behavioural. A button and a dropdown trigger that looks like a button are different components, even if they share a visual language. Forcing that into one component with an isDropdown prop is the kind of decision that ages badly.
The practical test: if a variant requires a conditional block inside the component that fundamentally changes what the component renders, it’s not a variant. It’s a different component sharing a visual language.
For teams navigating this across a growing library, design systems for small teams has a useful framing for when to split versus when to extend.
The Size and Colour Problem
Size and colour are where many component APIs get cluttered. Early versions add size="sm", size="md", size="lg". Later someone needs an extra-small. Then a medium-large. Then a size that’s only used on mobile. The enumeration grows until it maps to nothing coherent.
Two approaches tend to be more durable:
Semantic sizing. Name sizes by their role, not their pixel value. size="compact", size="default", size="spacious" ages better than size="12". When the underlying values change, the semantic names don’t need to.
Tokens through context. In more mature systems, size and colour flow through context or CSS custom properties rather than props. A <Button> inside a <Toolbar> knows it should be compact. This reduces prop noise but requires a deliberate system around it. Worth the investment when building a library used at scale.
Colour follows the same logic. A component accepting a raw hex value as a prop is a maintenance problem. Colour should map to named roles from your token set: intent="danger", intent="success", intent="neutral". The mapping from intent to actual colour lives in the token layer, not in the component. This separation matters for theming and for consistency, and it’s also where the design handoff to engineering process can break down if token naming between design and code is out of sync.
Escape Hatches, Used Carefully
Some flexibility is necessary. Teams that build components with no escape hatches end up with workarounds that are worse than the original problem.
The className prop (or style in some systems) is a reasonable escape hatch. Accepting an as or asChild prop to change the underlying element is common and useful. The escape hatch acknowledges that you can’t predict every use case.
What you should resist: escape hatches for things that really should be first-class props, or for functionality that suggests the component is doing too much. If every real-world use of a component passes overrides to change the same three properties, those overrides should be props.
Document the escape hatches explicitly. Make it clear they’re for edge cases, not the happy path. Teams that don’t do this end up with the escape hatch as the default usage pattern, which means the component API effectively doesn’t exist.
How Strynal approaches component API design
Component API design sits at the intersection of design systems and developer experience. Getting it right means the design system actually gets used. Getting it wrong means it becomes a liability. The problem looks small early and compounds fast.
At Strynal, API design is part of the UI/UX service from the start. We don’t hand off a component library and leave a team to discover its rough edges in production. The API decisions (naming, variant strategy, escape hatches, token integration) are made explicitly and documented as part of the deliverable.
If you’re building or auditing a design system and the component APIs have become a source of friction, get in touch. We can work through what’s there, identify where the contracts are breaking down, and help build something the team actually reaches for.