How to Design React Component APIs with Variants and Slots

Expose a limited set of visual choices through a variant prop and let callers fill named regions with slots, so a component stays small and hard to misuse.

7 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.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.

App.jsxApp.jsx
<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.

Library-specific pattern

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.

App.jsxApp.jsx
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 asChild behavior.
Rune AI

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.
RunePowered by Rune AI

Frequently Asked Questions

Should every component have a variant prop?

No. Add variants only when a component has a small set of real visual or behavioral choices. A component with one look needs no variant prop.

Are slots the same as the children prop?

The children prop is one implicit slot. Named slots are explicit props such as header or footer, used when a component has several distinct regions to fill.

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.