Ariakit
/

Composition

The render prop makes Ariakit components more versatile, allowing you to easily replace the default HTML element or enhance its features with custom components.

Changing the HTML element

The render prop lets you specify a different HTML element to be rendered instead of the default element.

For instance, the Textarea with inline Combobox example uses the render prop to replace the default input element rendered by the Combobox component with a textarea element. You can pass specific textarea props to the element directly:

<Combobox render={<textarea rows={5} />} />

The Combobox with links example renders the ComboboxItem component as an anchor element. Specific props should be passed to their corresponding components:

render={<a href="https://google.com" />}
/>

The Dialog with details & summary example uses the render prop to replace the default button element of the Button component with a summary element:

<Button render={<summary />}>Show modal</Button>

Composing with custom components

Besides changing the underlying HTML element, the render prop can be used to render a custom component.

In the Menu with Framer Motion example, the render prop is used to render the Menu component as a motion.div element:

<Menu render={<motion.div animate={{ y: 100 }} />} />

The Tab with Next.js App Router example uses the render prop to render the Tab component as the custom Link component from Next.js:

<Tab render={<Link href="/new" />}>New</Tab>

Merging the rendered element props

When passing an HTML element to the render prop, all the HTML props returned by the original component will be passed to the rendered element. The style, className, ref and event props will be automatically merged. In all other cases, the rendered element props will override the original component props.

  • Props coming from the original component will be passed to the rendered element:

    <ComboboxItem id="item" render={<a />} />
    <a id="item">...</a>
  • Props passed directly to the rendered element will override the original component props:

    <ComboboxItem id="item" render={<a id="link" />} />
    <a id="link">...</a>

This also applies to the children prop. You don't need to nest children within the render prop. But if you do, the children passed to the rendered element will override the original component children.

The same logic applies to custom components, but only for the props that are directly passed to the element within the render prop. The custom component itself is responsible for merging these props within its implementation. For more details, see Custom components must be open for extension.

Explicit render function

In addition to accepting a React element, the render prop can also receive a function that takes the original component's HTML props as a parameter and returns a React element. This provides you with greater control over the process of merging props:

<Button render={(htmlProps) => <summary {...htmlProps} />} />

This is useful when you want to compose the original component with custom components that accept HTML props through a custom prop:

<ComboboxItem render={(props) => <MyModal triggerProps={props} />} />

Another use case for the render function is to render elements between an internal wrapper and the rendered element. This is how the Dialog with scrollable backdrop example renders a custom backdrop element that wraps the dialog, but is still inside the dialog portal:

backdrop={false}
render={(dialogProps) => (
// Renders a custom backdrop element that wraps the dialog, but is still
// inside the dialog portal.
<div className="backdrop">
<div {...dialogProps} />
</div>
)}
>

Merging the render function props

When a function is passed to the render prop, the HTML props will not be automatically merged into the returned React element. It's your responsibility to merge them within the function itself, following the same approach described in Custom components must be open for extension.

With this in mind, it's generally recommended to pass all the HTML props you intend to merge to a transitional Role element passed directly to the render prop, as it can handle the merging process for you in a type-safe manner:

// ❌ Manually merging props
<Tab
render={(props) => (
<Link
{...props}
ref={mergeRefs(props.ref, myRef)}
onClick={(event) => {
props.onClick?.(event);
handleClick();
}}
/>
)}
/>
// ✅ Better
<Tab
render={
<Role.a
ref={myRef}
onClick={handleClick}
render={(props) => <Link {...props} />}
/>
}
/>

Custom components must be open for extension

When using the render prop with a custom component, you must ensure the component is open for extension. This means it should pass the incoming props, including event listeners and the forwarded ref prop, to the underlying element. Otherwise, the component may not work as expected.

This is a common pattern in most modern component libraries, so you shouldn't have problems with them. If you're using your own custom components, make sure they're open for extension by following these guidelines:

  1. 1

    Spread all props onto the underlying element.

  2. 2

    Forward the ref prop and merge it with the internal ref, if any.

  3. 3

    Merge the style and className props with the internal styles and classes, if any.

  4. 4

    Chain the event props with the internal event handlers, if any.

import { forwardRef, useRef } from "react";
import { mergeRefs } from "react-merge-refs";
const CustomButton = forwardRef(function CustomButton(props, forwardedRef) {
const internalRef = useRef(null);
return (
<button
{...props}
ref={mergeRefs([internalRef, forwardedRef])}
style={{
position: "relative",
...props.style,
}}
onClick={(event) => {
props.onClick?.(event);
// ...
}}
/>
);
});

Next steps

Continue reading our Guide to learn more about Ariakit:

Stay tuned

Join 1,000+ subscribers and receive monthly tips & updates on new Ariakit content.

No spam. Unsubscribe anytime. Read latest issue