React component API design starts with a simple goal: make a component small, predictable, and hard to misuse. Variants expose a limited set of visual choices through one prop, while slots let callers fill named regions with their own JSX.
Variants: one prop, a closed set of choices
A variant prop replaces a pile of boolean and style props with a single named choice. The component maps each variant to its class names, so callers never hand-write the styling. The mapping lives in one place, so changing a style touches one file.
const variants = {
primary: "button-primary",
secondary: "button-secondary",
danger: "button-danger",
};
function Button({ variant = "primary", children, ...props }) {
const className = variants[variant];
return (
<button className={className} {...props}>
{children}
</button>
);
}A caller passes the danger variant to the Button and gets the danger style without knowing which class names make it up. The default keeps the common case short. The caller reads the variant name and knows the result without opening the component body.
Keep the variant set closed
A closed variant set is the point of the pattern. It guarantees that every button in the app is one of three agreed looks, so the design stays consistent and the component stays testable.
Do not turn the component into a general-purpose className passthrough for every variation. When a one-off style is needed, a one-off component or a small wrapper is clearer than adding a tenth variant. The closed set is also what keeps autocomplete useful and what lets a design system document every supported look.
Some components need two dimensions, such as a variant and a size. Keep them as separate props instead of multiplying every combination into the variant list. A Button with variant and size stays small, while a variant list that enumerates every combination grows into a wall of names.
A variant prop is also a natural place for a TypeScript union type, which gives callers autocomplete and catches typos at compile time.
Slots: named regions for composition
When a component has several distinct regions, give each one a named slot instead of a long prop list. A card exposes header, body, and footer slots that callers fill with their own JSX. Each slot is just a prop that happens to hold JSX.
function Card({ header, body, footer }) {
return (
<section className="card">
{header && <header className="card-header">{header}</header>}
<div className="card-body">{body}</div>
{footer && <footer className="card-footer">{footer}</footer>}
</section>
);
}The card owns only its layout. Callers decide what each region contains. A slot that is omitted renders nothing, so a card without a footer drops the region instead of leaving an empty border.
<Card
header={<h2>Plan details</h2>}
body={<p>Billed monthly.</p>}
footer={<Button variant="secondary">Change plan</Button>}
/>Slots are JSX passed as props, which is the same idea as How to Pass JSX as a Prop in React. The single-region version is the children prop. A named slot that accepts JSX stays flexible because the caller can pass text, a component, or a fragment.
The asChild slot pattern
Some component libraries expose an asChild prop that lets a wrapper render as its child instead of its own tag. The child then receives the merged props, so a Button can become a link without a second component.
The asChild pattern is provided by libraries such as Radix UI, not by React itself. The Radix Slot utility merges props and refs onto the immediate child.
import { Slot } from "radix-ui";
function Button({ asChild, children, ...props }) {
const Comp = asChild ? Slot.Root : "button";
return <Comp {...props}>{children}</Comp>;
}With this Button, rendering an anchor as the child produces a link that keeps the button's styles and handlers. The utility is a library convenience, not something React requires; you only reach for it when one component must swap between tags. For a TypeScript version of this kind of flexible component, see How to Build Polymorphic React Components with TypeScript.
Design checklist
A useful mental test is to write the call site before the implementation.
- Add a variant prop only when a component has a small, real set of choices.
- Give each named region a slot instead of a prop per field.
- Use the children prop for the single main region.
- Reach for a library slot utility only when you need the
asChildbehavior.
Rune AI
Key Insights
- A variant prop exposes a limited, predictable set of choices.
- Keep the variant set closed instead of accepting every style as a prop.
- Named slots let callers fill distinct regions with their own JSX.
- The children prop is the simplest single slot.
- The asChild slot pattern is a library convention, not built into React.
Frequently Asked Questions
Should every component have a variant prop?
Are slots the same as the children prop?
Conclusion
A clean component API exposes a closed set of variants and a small number of named slots. Variants constrain the choices callers can make, and slots keep composition flexible without a long prop list.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.