Every prop you add is a maintenance commitment. Every abstraction you choose shapes how dozens of engineers think about your UI. When you’re building components that will be consumed across multiple teams and potentially multiple frameworks, the API surface is the single most consequential decision you’ll make.
I’ve been through this process multiple times now, most recently when building a shared component system using native web components that needed to work across a multi-brand platform. The lessons compound.
Start with the consumer, not the implementation
The most common mistake I see is designing component APIs from the inside out. You start with the implementation details and expose them as props or attributes. The result is components that are powerful but bewildering, with dozens of configuration options that mirror internal state.
Instead, start by writing the code you wish you could write. Mock up the ideal usage in a consuming application. What would the simplest, most intuitive API look like for the developer who just needs a button, a modal, or a data table?
This is especially important when working with web components, where the API surface is defined by attributes, properties, slots, and custom events. The browser’s component model gives you clear primitives to work with, but it’s easy to overload any of them.
Why we chose native web components
When your component system needs to work across multiple applications that might use different frameworks, or even no framework at all, native web components are a compelling choice.
The Shadow DOM provides true style encapsulation. Your component’s styles don’t leak out, and the consuming application’s styles don’t leak in. This matters enormously in a micro-frontend architecture where multiple teams are composing their own applications from shared building blocks.
Custom elements give you a clean registration model. You define your element once, and it works anywhere HTML works. No framework adapter layer, no wrapper components, no compatibility shims. The browser does the work.
Slots provide a natural composition mechanism. Instead of passing complex configuration objects as properties, consumers put their content directly into named slots. This is closer to how HTML actually works, and it makes the component’s usage obvious at a glance.
The tradeoff is that web components have a steeper initial development curve compared to framework-specific components. The lifecycle callbacks are different from React or Vue. Reactivity requires explicit implementation. Server-side rendering support, while improving, is still behind framework components. You need to weigh these costs against the benefits of framework independence.
The rule of three
Before abstracting a pattern into a configurable prop, I want to see it needed in at least three distinct contexts. Two occurrences might be coincidence. Three is a pattern worth supporting.
This discipline prevents the most expensive kind of tech debt: premature abstraction. A variant attribute with two options is manageable. A variant attribute with twelve options, half of which are used by a single consumer, is a liability that every future developer needs to understand and maintain.
When building components for a multi-brand platform, the temptation to add brand-specific variants is strong. Resist it. Instead, expose design tokens and CSS custom properties that let each brand customise the appearance without adding complexity to the component’s API.
Composition over configuration
The biggest shift in modern component design is the move from configuration (one component with many props) to composition (multiple small components assembled together).
Consider a card component. The configuration approach gives you attributes for title, subtitle, image, badge, footer, variant, and eventually you hit a combination that doesn’t fit any of your existing options. You add another variant. The component grows. The documentation grows. The test matrix grows.
The composition approach gives you card, card-header, card-body, card-footer as separate elements. With web components, these compose naturally through slots:
<my-card>
<my-card-header slot="header">
<my-badge>New</my-badge>
<h3>Card Title</h3>
</my-card-header>
<my-card-body>
<p>Whatever content the consumer needs.</p>
</my-card-body>
</my-card>
The consumer has full control over what goes where. The component doesn’t need to anticipate every possible layout. This pattern scales where configuration doesn’t.
Make the easy thing correct
A good component API makes the default behaviour the correct behaviour. If most consumers need a button to be type="button" rather than type="submit", that should be the default. If a modal should trap focus, focus trapping should be on by default. If a form input needs an associated label for accessibility, the component should enforce that relationship.
The best APIs are the ones where engineers can’t accidentally build something inaccessible or broken without explicitly opting out of the safe default.
This is where web components have an advantage that’s easy to overlook. Because you control the Shadow DOM, you can enforce accessibility patterns internally. The component can add ARIA attributes automatically, manage focus order, and handle keyboard interactions without relying on the consumer to remember.
Event design matters as much as attribute design
In a web component system, custom events are your primary mechanism for communicating changes back to the consuming application. The design of your event API is just as important as your attribute API.
Be specific about event names. change is too generic. selection-changed tells the consumer exactly what happened. Include relevant data in the event detail. And be deliberate about whether events should bubble and whether they should be composed (crossing shadow DOM boundaries).
One pattern that works well: mirror the platform’s conventions. If the browser’s native input element dispatches an input event on every keystroke and a change event on commit, your custom input component should follow the same pattern. Consumers shouldn’t need to learn a new mental model.
Document with examples, not descriptions
Every component should have at least three documented examples: the most common use case, a composition example showing how it works with other components, and an edge case that demonstrates the component’s boundaries.
A props table is necessary but insufficient. Engineers don’t read props tables when they’re in flow. They look for an example that’s close to what they need, copy it, and modify it. Make sure the examples are copy-paste ready and that they demonstrate real patterns from your actual codebase.
The long game
Component API design is product design for developers. The same principles apply: understand your users, optimise for the common case, and resist the urge to add complexity before the need is proven.
Every component you ship is a contract. Breaking changes in component APIs have a blast radius proportional to the number of consumers. When your components are used across multiple applications, multiple brands, and multiple teams, that blast radius is large. Design your APIs with that in mind, and future you will be grateful.