Composition
The as
and render
props make Ariakit components more versatile, allowing you to easily replace the default HTML element or enhance its features with custom components.
as
The as
prop lets you specify a different HTML element or a custom component to be rendered instead of the default element.
HTML elements
For instance, the Textarea with inline Combobox example uses the as
prop to replace the default input
element rendered by the Combobox component with a textarea
element. This allows specific textarea
props to be passed:
<Combobox as="textarea" rows={5} />
The Combobox with links example employs the as
prop to render the ComboboxItem
component as an anchor element:
<ComboboxItem as="a" href="https://google.com" />
The Dialog with details & summary example uses the as
prop to replace the default button
element of the Button component with a summary
element:
<Button as="summary">Show modal</Button>
Custom components
In the Menu with Framer Motion example, the as
prop is used to render the Menu component as a motion.div
element, allowing you to pass specific motion.div
props to the Menu
component:
<Menu as={motion.div} animate={{ y: 100 }} />
The Tab with Next.js App Router example uses the as
prop to render the Tab component as the custom Link
component from Next.js:
<Tab as={Link} href={{ pathname: "/" }} />
Prop conflicts
Although the as
prop is a convenient way to replace the default HTML element, it has one important caveat when using custom components: prop name conflicts.
For example, if you're combining two components that accept the same custom prop, only the original component will receive the prop:
<TooltipAnchor
// We can't pass the custom `store` prop to `MenuButton` here because it will
// be consumed by `TooltipAnchor` and won't be passed down to `MenuButton`.
as={MenuButton}
store={tooltip}
/>
In this case, it's preferable to use the render
prop instead.
render
The render
prop allows you to render a different HTML element or a custom component passing the original component's HTML props to it. This way, you can avoid prop conflicts:
<TooltipAnchor
store={tooltip}
render={(props) => <MenuButton store={menu} {...props} />}
>
Open menu
</TooltipAnchor>
If you're working with custom components that accept HTML props through a different prop name, this approach lets you remap the props:
<AriakitComponent render={(props) => <CustomComponent htmlProps={props} />} />
Nesting children
Flat is better than nested
With the render
prop, you don't need to nest the children
inside the render function. Instead, you can pass the children
as usual to the original component. The render function will receive the children
as a prop:
// ❌ Nesting
<TooltipAnchor
store={tooltip}
render={(props) => (
<MenuButton store={menu} {...props}>
Open menu
<MenuButtonArrow />
</MenuButton>
)}
/>
// ✅ Better
<TooltipAnchor
store={tooltip}
render={(props) => <MenuButton store={menu} {...props} />}
>
Open menu
<MenuButtonArrow />
</TooltipAnchor>
This approach also makes it easier to refactor a component to use the render
prop when needed. You can simply add the render
prop to the component without having to touch the children
.
Custom components must be open for extension
When using the as
or render
props with a custom component, it must be open for extension. This means the component 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.
import { forwardRef, useRef } from "react";
import { mergeRefs } from "react-merge-refs";
const CustomButton = forwardRef((props, forwardedRef) => {
const ref = useRef(null);
// Do something with the internal ref
return (
<button
// Spread the props
{...props}
// Merge the refs or pass the forwarded ref directly if you don't need
// an internal ref
ref={mergeRefs([ref, forwardedRef])}
// Merge the styles
style={{
position: "relative",
...props.style,
}}
onClick={(event) => {
// Call the original event handler if it exists
props.onClick?.(event);
// Do something else
}}
/>
);
});
This is a common pattern in most modern third-party libraries, so you shouldn't have problems with them. If you're using your own custom components, make sure they're open for extension.