Radix UI's Slot: The asChild Pattern and Element Substitution
What you learn in this chapter
You learn about radix-ui's Slot and the asChild pattern. This is a way to swap the rendered HTML element while preserving the look (the styling). It leads to "composition", an important design-system idea for combining components flexibly.
The problem: combining semantic HTML with styling
On the web, it is important to use the HTML element that matches the meaning (semantic HTML). Use <a> for navigation, <button> for submission, and so on.
However, when you try to build "a link with Button's design", every approach runs into a problem.
// ❌ Hand-write Button's classes on <a> → it cannot keep up with changes to Button
<a href="/profile" className="inline-flex items-center rounded-md bg-primary ...">
Profile
</a>
// ❌ <a> inside Button → <a> inside <button> is invalid HTML
<Button><a href="/profile">Profile</a></Button>
// ❌ Add href to Button → Button ends up carrying support for every element
<Button href="/profile">Profile</Button>
The solution: the asChild pattern
asChild is a mechanism that delegates the element to render to a "child element" while preserving the styling.
// ✅ asChild: it looks like Button, but the actual element is <a>
<Button variant="outline" asChild>
<a href="/profile">Profile</a>
</Button>
// The output HTML
<a href="/profile" class="inline-flex items-center rounded-md border ...">
Profile
</a>
How Slot works
The true identity of asChild is radix-ui's Slot. Slot is "a carrier that renders nothing itself and transfers the received props to the child element".
<Button asChild className="..." ref={ref}>
<a href="/profile">Profile</a>
</Button>
↓ inside Button, Comp = Slot.Root
<Slot.Root className="..." ref={ref}>
<a href="/profile">Profile</a>
</Slot.Root>
↓ what Slot does
1. Slot itself renders nothing
2. It receives the props passed from the parent
3. It finds the single child element
4. It merges the props into the child element
5. It renders only the child element
↓
<a href="/profile" class="..." ref={ref}>Profile</a>
The props merge rules
Slot merges the parent's props and the child element's props intelligently.
| Kind of props | Merge method |
|---|---|
className | Concatenate both (space-separated) |
style | Merge the objects |
Handlers such as onClick | Call both in order (child → parent) |
| Everything else | The child element's value takes precedence |
ref | Forward it to the child element (works as-is in React 19) |
<Slot.Root className="parent" onClick={parentHandler}>
<a className="child" onClick={childHandler} href="/link">Click</a>
</Slot.Root>
// The result
<a
className="parent child"
onClick={(e) => { childHandler(e); parentHandler(e) }} // the child's handler first, the parent's (Slot side) handler after
href="/link"
>
Click
</a>
Hands-on: using Slot
Step 1: a basic Slot
Import Slot from radix-ui and use Slot.Root.
import { Slot } from "radix-ui"
function Card({ asChild, children, ...props }) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp className="rounded-lg bg-card p-4 shadow" {...props}>
{children}
</Comp>
)
}
// Normal (rendered as a div)
<Card>This is a card</Card>
// => <div class="rounded-lg bg-card p-4 shadow">This is a card</div>
// asChild (rendered as an article)
<Card asChild>
<article>This is an article card</article>
</Card>
// => <article class="rounded-lg bg-card p-4 shadow">This is an article card</article>
Step 2: Button as a link
import { Button } from "@/components/ui/button"
function Navigation() {
return (
<nav>
{/* a normal button */}
<Button onClick={() => console.log("clicked")}>Click</Button>
{/* a button as a link */}
<Button variant="outline" asChild>
<a href="/about">About us</a>
</Button>
</nav>
)
}
Step 3: forwarding ref
Slot forwards ref to the child element too.
function FocusableLink() {
const linkRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
linkRef.current?.focus()
}, [])
return (
<Button asChild>
<a href="/focused" ref={linkRef}>This is focused automatically</a>
</Button>
)
}
The Button's (pre-substitution element's) ref type stays HTMLButtonElement, so write the substituted element's ref on the child element. If you pass it on the parent side, Slot still forwards it to the child, but you get a type error.
The benefits asChild brings to a design system
asChild is an important pattern that supports "composition" in a design system.
1. Consistent design
<Button>Normal button</Button>
<Button asChild><a href="/link">Link button</a></Button>
<Button asChild><Link href="/next">Router link</Link></Button>
They all share the same Button design; only the inner content (the element) differs.
2. Preserving semantic HTML
It looks like a Button, but semantically you can use the correct <a>. This produces correct markup for SEO and accessibility.
3. Separation of concerns
Button is responsible only for "the look", and the consumer decides "what element it is". This is exactly the design-system principle of "single responsibility".
4. Extensibility
Even when a new link component arrives (Next.js's Link, React Router's Link, and so on), no changes to Button are needed.
asChild in Button
This one line in the Button from the final-form chapter was the key.
function Button({ className, variant, size, asChild = false, ...props }) {
const Comp = asChild ? Slot.Root : "button" // ← here
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
asChild = false (default) asChild = true
↓ ↓
Comp = "button" Comp = Slot.Root
↓ ↓
<button class="..."> <a class="..."> (replaced by the child element)
Caveats
Only one child element
Slot can receive only a single child element.
// ❌ Error (two children)
<Button asChild>
<span>Icon</span>
<span>Text</span>
</Button>
// ✅ OK (group them into one element)
<Button asChild>
<a href="/link">
<span>Icon</span>
<span>Text</span>
</a>
</Button>
The child must be a React element
// ❌ Error (plain text)
<Button asChild>Just text</Button>
// ✅ OK
<Button asChild>
<span>Just text</span>
</Button>
Chapter summary
- asChild is a mechanism that swaps the element while preserving the styling
- Its true identity is
radix-ui'sSlot(useSlot.Root) - Props are merged intelligently (
classNameconcatenation, handler chaining,refforwarding) - You can combine semantic HTML and design = achieving composition
- The child must be a single React element
In the next chapter, you learn about the TypeScript types that support the code so far.