Component Design: From Primitive to Product
The best component APIs are not the ones with the most props. They are the ones that vanish - you reach for them without thinking because the shape matches your mental model.
Article focus
3 composition patterns
Slots, compound, render props
Key takeaways
- Primitives own look, product components own intent, pages own wiring.
- Compound components beat giant props objects for complex UI.
- If a component has more than 8 props, you probably missed a composition opportunity.
What separates a good component API from a bad one?
Good APIs vanish. You reach for them by instinct because the API shape mirrors your mental model of the UI.
I have used button libraries with forty props. I have also used ones with four. The four-prop version was not less capable - it just understood that a button is a trigger, not a Swiss Army knife.
The best component APIs share a property: they are obvious. You do not read the docs to guess whether the prop is called variant, kind, or appearance. You know it is variant because every component in the system calls it the same thing.
This does not happen by accident. It happens when you design components around intent, not around flexibility.
Component Layer Explorer
Click each layer to see examples and rules for primitives, product components, and pages.
Primitives
Handle the look. No business logic. Pure UI building blocks.
Examples
ButtonCardInputSelectModalRules
- No business logic
- No data fetching
- Pure presentational
Click each layer to explore. Primitives → Product Components → Pages.
Primitives, product components, and pages - a refresher
Primitives handle look (Button, Card). Product components handle meaning (PricingTierCard). Pages wire things together.
In the Frontend Architecture pillar post I introduced the three-layer model. Here I want to go deeper on how the boundaries actually play out in code.
Primitives are your design tokens turned into components. They have no business logic. A Button does not know what happens on click - it just renders the right border radius and hover state. A Card does not know what content it holds - it provides the shell.
Product components are where meaning enters. A BillingAddressForm is not a generic form. It knows about addresses, validation rules, and the checkout flow. It composes primitives internally but exposes domain-level props.
Pages are the wiring layer. They fetch data, handle route params, and compose product components. If a page has complex conditional rendering or business logic, that logic probably belongs one layer down.
When do I reach for compound components?
When a UI unit has multiple moving parts that share implicit state - dropdown, tabs, accordion, select.
Compound components are my favorite pattern for complex UI because they let the consumer control layout while the parent controls state.
The magic is that the consumer can rearrange the DOM freely - put Tabs.Content before Tabs.List if they want - and everything still works because the parent communicates through React context, not through DOM position.
The implementation is straightforward. Tabs creates a context with the value and an onChange handler. Tabs.Trigger and Tabs.Content consume that context. No prop drilling, no prop forwarding, no layout constraints.
tsx
<Tabs defaultValue="design">
<Tabs.List>
<Tabs.Trigger value="design">Design</Tabs.Trigger>
<Tabs.Trigger value="code">Code</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="design">
Content about design
</Tabs.Content>
<Tabs.Content value="code">
Content about code
</Tabs.Content>
</Tabs>Slots - the pattern that removes the most props
Slots (children, or named slots via props) let consumers inject arbitrary content without adding a prop for every variant.
The fastest way to reduce prop count is to replace optional content props with slots. Instead of:
The slot version is longer but more flexible. You can add an icon, change the order, conditionally render parts - all without touching the Card component. The Card just provides the shell and the layout grid.
I apply the 8-prop rule: if a component needs more than 8 props, I look for composition opportunities. Usually I find 2-3 props that could be slots instead.
tsx
// Before - every variant needs a new prop
<Card
title="Pro Plan"
description="Best for teams"
ctaLabel="Upgrade"
ctaHref="/pricing"
badgeText="Popular"
badgeVariant="success"
footerText="Cancel anytime"
/>
// After - slots handle everything custom
<Card>
<Card.Badge>Popular</Card.Badge>
<Card.Title>Pro Plan</Card.Title>
<Card.Description>Best for teams</Card.Description>
<Card.Cta href="/pricing">Upgrade</Card.Cta>
<Card.Footer>Cancel anytime</Card.Footer>
</Card>Render props and the line between power and noise
Render props are useful when the consumer needs full control over rendering. But most of the time, slots or compound components are cleaner.
Render props give the consumer maximum control at the cost of readability and indentation. I reach for them sparingly - typically when building generic data components (autocomplete, data table) where the rendering pattern varies wildly.
For most UI components, slots and compound components cover 95% of use cases. Reserve render props for the cases where the consumer genuinely needs to override the entire rendering, not just customize content.
A decision tree for component API design
Fixed content → props. Variable content → slots. Shared implicit state → compound. Full rendering control → render props.
When designing a new component, I run through this quickly:
Most components never need to go past step 2. The ones that do are usually data-display components, not layout or action components.
- Is the content fixed? (a button label) → simple prop.
- Is the content variable but the layout is fixed? (card body, modal content) → children slot.
- Are there multiple movable pieces sharing state? (tabs, dropdown, accordion) → compound components.
- Does the consumer need to override the entire rendering? (data table row) → render prop.
Need help implementing this?
I build these systems for a living - let's work on yours.
Frontend Development
Production UIs in React, Next.js, or Vue- third-party and payment integrations included. Built for real traffic and maintained in production.
Frontend Architecture & System Design
Structure so teams can ship. Clear boundaries, state strategy, and contracts. New features land cleanly; refactors stay low-risk.
Product-Focused UI Engineering
Figma and specs to production. Accessible, consistent components and design-system alignment so what ships matches design and works for everyone.
Your turn
- >Did this help you ship something?
- >Which part clicked the most for you?
- >Applying this at work? Share your experience.
Recommended blogs
Continue reading

Frontend Architecture: The Mental Model That Keeps Complex Apps Maintainable
Five interconnected pillars - components, state, contracts, testing, tooling - that keep a frontend codebase healthy as the team and features grow.
Photo by ThisIsEngineering on Pexels
Read article
How React Knows What to Re-render: The Reconciliation & Diffing Algorithm
Learn React's reconciliation algorithm: virtual DOM diffing, key prop rules, render vs commit phases, React 18 batching, and concurrent rendering - from first principles to production patterns.
Photo by Joshua Plattner:
Read article
Discussion
Leave a comment
Thoughts, questions, corrections - all welcome.