Capítulo 7 de 51

Chapter 7: Composition

Core Idea

Base UI's universal composition mechanism is the render prop (not asChild — the naming used by Radix and older libraries): pass a React element to replace the rendered output, nest it across multiple components as deep as needed, or pass a function (props, state) => element for full control and state-driven rendering.

Key Concepts

  • render with an element: <Menu.Trigger render={<MyButton size="md" />}>Open menu</Menu.Trigger> — the custom component must forward ref and spread all received props onto its underlying DOM node (same contract as Radix's asChild, different prop name).
  • Nesting: render props nest arbitrarily deep — the documented pattern for composing Tooltip.Trigger + Dialog.Trigger + Menu.Trigger + a custom button all onto one element is chaining render={<Dialog.Trigger render={<Menu.Trigger render={<MyButton />} />} />}.
  • Changing the default element: same render prop changes what HTML tag a part outputs — e.g. <Menu.Item render={<a href="..." />}> turns a <div>-rendering item into an anchor. Each part renders the most semantically appropriate element by default; override case-by-case, not globally.
  • Render function form: render={(props, state) => <span {...props}>{state.checked ? <A/> : <B/>}</span>} — for performance-sensitive cases, gives full control over prop spreading and lets output vary by the component's live state (e.g. Switch.Thumb swapping an icon based on state.checked).

Code Examples

/* Element form — swap the rendered component */
<Menu.Trigger render={<MyButton size="md" />}>Open menu</Menu.Trigger>
/* Nested composition across three components + a custom button */
<Dialog.Root>
  <Tooltip.Root>
    <Tooltip.Trigger render={
      <Dialog.Trigger render={
        <Menu.Trigger render={<MyButton size="md" />}>Open menu</Menu.Trigger>
      } />
    } />
    <Tooltip.Portal>...</Tooltip.Portal>
  </Tooltip.Root>
  <Dialog.Portal>...</Dialog.Portal>
</Dialog.Root>
/* Function form — state-driven rendering */
<Switch.Thumb render={(props, state) => (
  <span {...props}>{state.checked ? <CheckedIcon /> : <UncheckedIcon />}</span>
)} />
  • What it demonstrates: render unifies three separate concerns (swap component, change tag, react to state) behind one prop, rather than three separate APIs.

Key Takeaways

  1. When integrating a design-system Button/Link component with Base UI triggers, use the element form of render — remember the target component must forward ref and spread props, or interactions silently break.
  2. Reach for the function form of render only when you need to branch output on the component's own state or you're in a proven performance-sensitive path — the element form covers the common case with less code.
  3. Coming from Radix Primitives: render replaces asChild conceptually, but the syntax differs — render={<X/>} instead of asChild + a child <X/>.

Connects To

  • ch005 (Styling): className/style function forms follow the same (state) => ... pattern as render's function form.
  • ch050 (useRender): the underlying hook that implements this render prop contract — read it before building a custom component meant to accept render itself.
  • radix-primitives-docs ch005-composition.md: the equivalent Radix concept (asChild), for side-by-side comparison.