When working with Radix UI (used under the hood in shadcn/ui), things are usually smooth — composable, accessible, and beautifully engineered.
But one day, I wrapped a DialogTrigger
with a Tooltip
and…
Nothing happened.
No errors. No warnings. Just a button that didn’t open the dialog. 🤯
Turns out, there's a common but sneaky mistake that trips up many devs — and it’s all about the order in which you nest your components.
❌ The Broken Version
This is what I initially wrote — trying to wrap my dialog trigger in a tooltip:
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline">
<EditIcon />
</Button>
</TooltipTrigger>
<TooltipContent>Edit podcast</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
{/* Form goes here */}
</DialogContent>
</Dialog>
At first glance, this looks totally fine.
But the dialog wouldn’t open when clicking the button.
The tooltip shows, but the dialog never appears.
🤔 What’s Going Wrong?
Let’s break this down.
Both DialogTrigger and TooltipTrigger from Radix use the asChild prop, which means:
They don’t render any DOM element themselves.
Instead, they pass event handlers like onClick to their child — which must be a real DOM element, like a .
In the broken example, DialogTrigger wraps Tooltip, which is not a DOM element — it's just a React component.
So the click event never reaches a usable target for Dialog.
âś… The Working Version
Here’s the corrected nesting:
<Dialog open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button size="icon" variant="outline">
<EditIcon />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>Edit podcast</TooltipContent>
</Tooltip>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
{/* Form goes here */}
</DialogContent>
</Dialog>
Why does this work?
- TooltipTrigger passes props down to DialogTrigger
- DialogTrigger passes props down to the final Button
- The Button becomes the single, shared DOM element that receives both behaviors
đź§ Rule of Thumb
Always place the outermost wrapper (like Tooltip) outside, and the final DOM element (like Button) inside both DialogTrigger and TooltipTrigger, using asChild.
Top comments (1)
Thanks!
In my case, I had to remove
asChild
. Both examples below work, but the first one has better accessibility score. I wanted to have a tooltip over the tabs.Example2: