Capítulo 5 de 43

Chapter 5: Composition

Core Idea

The asChild prop lets any Radix part clone its child element instead of rendering its own default DOM element — this is how you swap the underlying tag or compose Radix's behavior onto your own React components.

Key Concepts

  • asChild: when true, Radix skips rendering its default element and instead clones the single child, injecting the required props/behavior into it.
  • Prop spreading requirement: a custom component used with asChild must spread all received props onto its underlying DOM node, or Radix's injected handlers/attributes are lost.
  • Ref forwarding requirement: a custom component used with asChild must forward its ref (React.forwardRef) since Radix sometimes needs the DOM node reference (e.g. to measure size).
  • Deep/nested composition: asChild can be chained — multiple primitives' triggers (e.g. Tooltip.Trigger + Dialog.Trigger) can all target the same underlying custom button.

Code Examples

import * as React from "react";
import { Dialog, Tooltip } from "radix-ui";

const MyButton = React.forwardRef((props, forwardedRef) => (
  <button {...props} ref={forwardedRef} />
));

export default () => (
  <Dialog.Root>
    <Tooltip.Root>
      <Tooltip.Trigger asChild>
        <Dialog.Trigger asChild>
          <MyButton>Open dialog</MyButton>
        </Dialog.Trigger>
      </Tooltip.Trigger>
      <Tooltip.Portal>…</Tooltip.Portal>
    </Tooltip.Root>
    <Dialog.Portal>...</Dialog.Portal>
  </Dialog.Root>
);
  • What it demonstrates: composing two independent primitives' trigger behavior onto one shared custom button via nested asChild.

Anti-patterns

  • Changing a trigger's element type without preserving interactivity: e.g. swapping Tooltip.Trigger's default button for a non-focusable div breaks keyboard/screen-reader accessibility — you own that responsibility once you use asChild.
  • A leaf component that doesn't spread props or forward refs: silently breaks Radix's injected behavior instead of erroring loudly, so this bug is easy to miss.

Key Takeaways

  1. Reach for asChild when you want a Trigger/part to render as your own design-system component instead of Radix's plain default element.
  2. Make prop-spreading and ref-forwarding a default habit for any "leaf" component in your design system, since Radix (and other libraries) will lean on asChild composition.
  3. Whenever you change the rendered element via asChild, you inherit responsibility for keeping it accessible.

Connects To

  • Accessibility: explains why element-type changes via asChild carry accessibility risk.
  • Slot (Utilities): the primitive that implements the underlying clone-child mechanism asChild relies on.