How does radix-ui implement the asChild prop?
Great question — the
asChild
prop in
Radix UI is
a clever design pattern that allows components to
render as a different element
(like a custom component or a native HTML tag)
while preserving all the behavior and accessibility
logic
of the original Radix component.
Let’s break it down and then look at how it’s implemented under the hood.
Normally, a Radix component (like
<Button>
,
<DialogTrigger>
, etc.) renders a specific element — for example:
jsx<Button>Click me</Button>
renders something like:
html<button class="...">Click me</button>
But if you use
asChild
, you
can
replace
that underlying DOM node with another component:
jsx<Button asChild> <a href="/home">Go home</a> </Button>
Now the
<a>
tag
renders instead of a
<button>
, but it
inherits all the behavior, accessibility attributes,
and event handling
from Button
.
The
asChild
prop
in Radix UI works using a special helper component called
Slot
from the package
@radix-ui/react-slot
.
Here’s the simplified core idea of
Slot
:
tsx// Pseudocode version of @radix-ui/react-slot import * as React from 'react' const Slot = React.forwardRef(({ children, ...props }, forwardedRef) => { if (React.isValidElement(children)) { // Clone the child and merge props + ref return React.cloneElement(children, { ...props, ref: forwardedRef ? mergeRefs(forwardedRef, children.ref) : children.ref, // Merge className or style if needed className: [props.className, children.props.className] .filter(Boolean) .join(' ') }) } // If no valid React element, render nothing or fallback return null })
The key trick is using
React.cloneElement()
to take the child you passed in (<a href="/home">Go home</a>
) and
inject Radix’s behavior/props
(like event handlers,
aria-*
attributes, etc.) into it.
In a Radix component, you’ll often see something like this:
tsximport { Slot } from '@radix-ui/react-slot' const Button = React.forwardRef( ({ asChild, ...props }, forwardedRef) => { const Comp = asChild ? Slot : 'button' return ( <Comp {...props} ref={forwardedRef} className="radix-button" /> ) } )
So when you do:
jsx<Button asChild> <a href="/home">Home</a> </Button>
it becomes:
jsx<Slot> <a href="/home">Home</a> </Slot>
And
Slot
merges
all the button’s logic into that
<a>
tag.
This pattern allows:
Composition flexibility — use your own HTML tags or custom components.
Single source of truth for behavior — Radix components don’t care what they render as, they just apply logic.
Type safety
— thanks to TypeScript and the
Slot
API, you still get autocomplete and correct prop
types.
asChild
toggles whether the component renders its own element
(like
button
)
or delegates rendering to the child via the
Slot
component.
The
Slot
component uses
React.cloneElement()
to pass down behavior, accessibility attributes, event
handlers, and refs.
This pattern gives developers maximum flexibility to integrate Radix components with their own UI primitives or design systems.
Would you like me to show a
fully working custom example
(i.e., implementing your own mini
asChild
system like Radix’s) so you can see it in action in React?