Problem
When wrapping a shadcn SidebarMenuButton with a TooltipTrigger, React throws a hydration error because both components render <button> elements, producing invalid nested HTML:
Unhandled Runtime Error
Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: In HTML, <button> cannot be a descendant of <button>.
This will cause a hydration error.
<TooltipTrigger>
<button>
<SidebarMenuButton>
<button>
The broken JSX that triggers the error:
<Tooltip>
<TooltipTrigger>
<SidebarMenuButton>
<Home className="size-4" />
<span>Dashboard</span>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right">Dashboard</TooltipContent>
</Tooltip>
Both TooltipTrigger and SidebarMenuButton render a <button> by default, creating a <button><button>...</button></button> structure in the DOM.
Solution
Option 1: Add asChild to TooltipTrigger (recommended)
<Tooltip>
<TooltipTrigger asChild>
{/* ^^^^^^^ delegates rendering to SidebarMenuButton */}
<SidebarMenuButton>
<Home className="size-4" />
<span>Dashboard</span>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right">Dashboard</TooltipContent>
</Tooltip>
Option 2: Render SidebarMenuButton as a non-button element
<Tooltip>
<TooltipTrigger>
<SidebarMenuButton asChild>
<div role="button" tabIndex={0}>
<Home className="size-4" />
<span>Dashboard</span>
</div>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent side="right">Dashboard</TooltipContent>
</Tooltip>
Option 1 is preferred because it keeps a single <button> in the DOM with correct semantics and no extra wrapper elements.
Why It Works
Radix UI primitives (used by shadcn/ui) support an asChild prop that swaps the component's default rendered element for a Slot. The Slot component merges all of the parent's props, event handlers, and refs onto the child element instead of wrapping it in an additional DOM node. When TooltipTrigger receives asChild, it stops rendering its own <button> and instead passes tooltip-related props (aria attributes, event listeners) directly onto the SidebarMenuButton. The result is a single <button> in the DOM with both tooltip trigger behavior and sidebar menu button styling.
Context
- shadcn/ui built on Radix UI primitives (
@radix-ui/react-tooltip,@radix-ui/react-slot) - React 18+ with SSR or any framework that performs hydration (Next.js, Remix)
- The same nested-button issue occurs with
DropdownMenuTrigger+Button,DialogTrigger+Button,PopoverTrigger+Button, and any other Radix trigger wrapping a button-based component - The HTML spec forbids interactive content nested inside
<button>elements, so this is also an accessibility violation caught by tools like axe-core - The
asChildpattern is documented in the Radix UI composition guide