Component libraries and UI infrastructure
Building components with clear responsibilities
Shared components stay flexible when their APIs preserve composition, focused behavior, and documented system variants.
Shared components break down when they try to anticipate every product scenario that might contain them. When a component library begins accepting isolated product requests, component APIs quickly bloat with boolean flags, internal conditionals multiply, and a generic building block turns into an unmaintainable feature framework.
A shared component is a focused contract. Its job is to make a stable design or interaction decision easy to use, while leaving product code responsible for the domain data and business logic it supplies.
A card owns its container, not its contents
A container component like a Card is the classic test of responsibility. A card's job is purely structural: it provides a border, elevation, padding, and the visual variants defined by the design system. It should have no idea whether it holds an account summary, an empty state, or an invoice table.
<Card appearance="outlined">
<AccountSummary />
</Card>
The danger begins with shortcut props like isAccountCard. Soon after, another team requests hasFooterAction, followed by statusBadge and approvalState. Within a few releases, the card is full of internal if/else branches and bespoke prop configurations for specific workflows. Keeping the component's API focused on system variants (borders, elevation, padding) leaves consumers free to compose whatever content they need inside it.
Composition preserves flexibility
More complex components often need internal structure without prescribing the content inside. A callout or alert banner, for example, coordinates status colors, icon placement, and dismiss mechanics, but it should not dictate the message text or the exact buttons rendered.
Compound subcomponents and named slots let the library manage accessibility and layout while leaving content decisions to feature teams.
<Callout appearance="warning">
<CalloutTitle>Payment overdue</CalloutTitle>
<CalloutBody>Invoices past 30 days are subject to late fees.</CalloutBody>
<CalloutActions>
<Button>Pay now</Button>
</CalloutActions>
</Callout>
This separation keeps the contract clean. The callout manages how elements are positioned and announced to assistive technology, without ever needing to inspect account balance models, verify user permissions, or fire the payment request itself.
High-level defaults still have a place
Not every component should require assembling multiple compound pieces by hand. A select dropdown involves a trigger, floating listbox, option semantics, keyboard navigation, and focus restoration. That composition is common enough, and tricky enough to build correctly, that a ready-made default is valuable.
The key is keeping that default focused on interaction mechanics rather than product features. A Select can manage selection states and keyboard navigation, but it should not fetch its own data or make assumptions about backend schemas. When a feature workflow diverges from that standard default, we can assemble the underlying primitives directly instead of forcing the shared control to absorb one-off configurations.