Slot Based Component APIs in React
AI generated
</>
{ }
React · Component Design · asChild
Slot Based Component APIs in React
understanding and implementing the asChild pattern yourself

Slot based component APIs replace a component's root element with exactly one passed child element instead of inserting an extra DOM element. Radix popularized this approach under the name asChild, but the underlying slot component can also be built yourself in a few lines without any third party library.

18 min read asChild · Props Merging · Ref Merging React 19 · TypeScript

1. What slot based component APIs actually solve

A slot based component API solves a problem that often shows up with the as prop: sometimes a component should not replace its root element by an element name, but by a complete, already existing child element with its own props. The classic case is a button that visually looks like a button but should actually be a React Router link with its own to prop and its own onClick, without an extra, redundant <button> being rendered around the link.

Without slot based component APIs, you would either have to accept two nested elements, which can produce invalid HTML, for example a link inside a button, or write a separate variant for every combination. The slot pattern solves this by transferring the parent component's props and behavior directly onto the single passed child element instead of rendering its own wrapper element. This pattern became known through Radix UI under the name asChild, but it works independently of any specific library.

2. The asChild prop core principle

The core principle of a slot based component API is that a component accepts a boolean prop called asChild. If asChild is set, the component no longer renders its own DOM element, but takes children as the only allowed React element and clones it with cloneElement, transferring all of the parent component's props onto the cloned element. If asChild is not set, the component renders its default element as usual, usually a <button> or <div>.

The decisive difference from the as prop is the direction of control: with the as prop, a string or component reference determines what gets rendered. With a slot based component API, the actually passed child element itself determines what gets rendered, including all of its own props. This makes slot components particularly suitable for cases where the target element is already fully configured with its own props, for example a router link with several specific attributes.


// Slot.jsx — minimal slot implementation, framework agnostic
import { cloneElement, isValidElement } from "react";

function Slot({ children, ...slotProps }) {
  if (!isValidElement(children)) {
    // Slot requires exactly one valid React element as child
    return null;
  }
  // Merge props: the child's own props win over the slot's props
  return cloneElement(children, {
    ...slotProps,
    ...children.props,
    className: [slotProps.className, children.props.className].filter(Boolean).join(" "),
  });
}

function Button({ asChild, children, className, ...rest }) {
  const Component = asChild ? Slot : "button";
  return (
    <Component className={`btn ${className ?? ""}`} {...rest}>
      {children}
    </Component>
  );
}

// Usage — Link keeps its own "to" prop, no extra wrapper element rendered
// <Button asChild>
//   <Link to="/pricing">View pricing</Link>
// </Button>

3. Building your own slot component without Radix

Whoever does not want to introduce a dependency on Radix can adopt the Slot component shown in the previous section directly and maintain it within their own design system. The core consists of three lines: check whether children is a valid React element, clone the element with cloneElement, and merge props while doing so. The actual complexity of a production ready slot based component API lies in the details of props merging, not in the core principle itself.

An important edge case concerns multiple child elements: if asChild is set and more than one child element is passed, for example text and an icon side by side, cloneElement fails because children is then no longer a single element but an array. A robust slot based component API checks this case explicitly and throws a clear error message in development mode, so the problem is noticed immediately instead of showing up as a silent rendering bug in production.


// Slot.jsx — with explicit validation for common misuse
import { Children, cloneElement, isValidElement } from "react";

function Slot({ children, ...slotProps }) {
  const childArray = Children.toArray(children);

  if (childArray.length !== 1 || !isValidElement(childArray[0])) {
    if (process.env.NODE_ENV !== "production") {
      console.error(
        "Slot expects exactly one valid React element as its child. " +
          "Received: " + childArray.length + " children."
      );
    }
    return null;
  }

  const child = childArray[0];
  return cloneElement(child, {
    ...slotProps,
    ...child.props,
  });
}

4. Named slots: multiple insertion points

A single asChild slot is enough for a component with exactly one replaceable element. More complex components often need several such insertion points at once, for example a card component with a replaceable header element and a replaceable footer element. This is where the named slots pattern comes in: instead of a single asChild prop, the component defines several named props such as headerAs and footerAs, or it detects special child components via a displayName to distribute them to the correct position in the internal markup.

The second, in practice more common variant for slot based component APIs with multiple insertion points is distributing children by type. A component scans its child elements, filters out for example all elements of type Card.Header, and renders them at a fixed position, while the rest is passed through normally. This technique combines the slot principle with the compound component pattern and works well for layout components with several clearly named areas.


// Card.jsx — named slots via child type detection
import { Children, isValidElement } from "react";

function Card({ children }) {
  const childArray = Children.toArray(children);
  const header = childArray.find((c) => isValidElement(c) && c.type === CardHeader);
  const footer = childArray.find((c) => isValidElement(c) && c.type === CardFooter);
  const body = childArray.filter(
    (c) => !(isValidElement(c) && (c.type === CardHeader || c.type === CardFooter))
  );

  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{body}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

function CardHeader({ children }) {
  return children;
}
function CardFooter({ children }) {
  return children;
}

Card.Header = CardHeader;
Card.Footer = CardFooter;
export { Card };

5. Props and ref merging for slots

The hardest detail of every slot based component API is correct props merging. Event handlers such as onClick must not simply be overwritten, because otherwise either the parent component's logic or the child element's logic gets lost. The correct approach composes both handlers into a new function that calls first one and then the other. The same applies to className, which must be concatenated rather than replaced, and to style, which should be merged as an object.

Ref merging is the second critical point. When both the parent component and the caller want to set a ref on the same element, the slot based component API needs a function that merges multiple refs and updates all affected refs on every ref change, regardless of whether they are function refs or object refs. Without this merging, either the internal or the external ref would be lost, which especially in libraries that rely on internal DOM access leads to hard to find bugs.

6. Slot pattern versus children as a function

An alternative to slot based APIs is the children as a function technique, where the component expects a function as children and passes state and handlers to that function as arguments. The caller then decides for themselves how the result is rendered. The difference from a slot based component API is that children as a function explicitly exposes state, while the slot pattern implicitly transfers props onto an existing element.

In practice, children as a function suits better when the component has state that the caller needs for rendering, for example the current loading state of a form. A slot based component API suits better when no additional state is needed, only an extra wrapper element should be avoided, for example a button that is actually a link. Both patterns can be used side by side in the same library, depending on the requirements of each component.

7. Accessibility for slot components

With a slot based component API, accessibility tends to be easier to ensure than with the as prop, because the element that actually gets rendered is fully dictated by the caller and thus already brings the correct semantics. A link stays a link with all native keyboard and screen reader properties, regardless of a button component adding visual classes and a click handler to it. No intermediate element is created that would need additional ARIA semantics.

One risk remains, though: if the slot component overwrites props such as role or aria-* attributes without reflection instead of merging them with those of the child element, the original semantics can be lost. A careful slot based component API treats ARIA attributes like className: merge instead of replace, and when in doubt, prefer the child element's values, because it is closer to the actual use case.

8. Common mistakes with slot APIs

The most common mistake with slot based component APIs is passing more than one child element while asChild is active. Since cloneElement only works with a single element, an array of child elements leads either to a runtime error or to silent misbehavior, depending on the implementation. A second common mistake is overwriting instead of composing event handlers, causing either the component's internal logic or the caller's logic to be lost without any error pointing to it.

A third mistake concerns TypeScript typing: because cloneElement can apply arbitrary props to an arbitrary element at runtime, you quickly lose type safety without additional generics. A robust slot based component API in TypeScript explicitly restricts the allowed child type to elements with compatible props, for example via a generic ReactElement<ComponentPropsWithoutRef<"button">>, instead of typing children as an arbitrary ReactNode.

9. The slot pattern compared to alternatives

The following table compares the slot pattern with the two other important approaches for flexible component APIs.

Approach Extra DOM element State exposable Typical use
Slot / asChild No No, props merging only Button as link, trigger as arbitrary element
as prop Yes, the chosen element itself No Heading or text with variable tag
Children as a function Optional, depending on return value Yes, explicitly as arguments Exposing form state, loading state

In practice, these three patterns complement each other: the slot pattern suits when an existing element should keep its own props unchanged. The as prop suits when only the element name should vary. Children as a function suits when state from the component needs to be passed to the caller. Many production ready component libraries combine all three depending on the building block.

Mironsoft

React component APIs and design systems

Need flexible slot based components for your design system?

We implement the slot pattern, props merging, and ref merging robustly and type safely, so your components combine with arbitrary child elements without losing accessibility.

API design

Combining slot, as prop, and children as a function appropriately

Props and ref merging

Robust merge logic without losing handlers or refs

TypeScript safety

Generic types for slot children instead of any

10. Summary

Slot based component APIs solve a specific problem in component design: a child element should take over the props of a parent component without an extra wrapper element appearing in the DOM. Radix's asChild pattern made this approach well known, but the underlying slot component can be implemented yourself with cloneElement and clean props merging in a few lines, without any additional dependency.

The hardest part is not the core principle but the details: event handlers must be composed rather than overwritten, refs must be merged, and in TypeScript the allowed child type should be explicitly typed. Whoever masters these details has, with slot based component APIs, a powerful tool to flexibly combine components with arbitrary child elements without sacrificing accessibility or type safety.

Slot Based Component APIs — The Essentials

Core principle

asChild replaces the root element with the passed child element, no wrapper is rendered.

Implementation

cloneElement plus clean props merging is enough for a custom slot component without Radix.

Merging

Compose event handlers instead of overwriting, merge refs with a dedicated merge function.

Accessibility

No intermediate element needed, child semantics stay intact when ARIA props are merged.

11. FAQ: Slot Based Component APIs

1What is a slot based component API?
The root element is replaced by a passed child element that takes over the parent's props.
2What does asChild do?
Clones the single child element with cloneElement and transfers its own props onto it, no wrapper.
3Do I need Radix for this?
No, a minimal slot component can be implemented yourself with cloneElement.
4What about multiple child elements?
cloneElement only works with one element, multiple lead to an error or silent misbehavior.
5How to merge event handlers?
Write a new function that calls both handlers one after another, instead of overwriting one.
6How does ref merging work?
A merge function updates all affected refs on every change, regardless of ref type.
7Difference to the as prop?
as selects an element by name, slot transfers props onto an already configured child element.
8When children as a function instead?
When state is needed for rendering, slot suffices for pure wrapper avoidance.
9How to type it cleanly in TypeScript?
Restrict the child type to a ReactElement with compatible props, not an arbitrary ReactNode.
10Does this affect accessibility?
Positively, since no intermediate element appears, provided ARIA attributes are merged not overwritten.