Skip to content

Fix nested button hydration error with shadcn/ui components

fix

React hydration error from nested buttons when wrapping shadcn SidebarMenuButton with TooltipTrigger

reactshadcnhydrationaccessibility
27 views

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 asChild pattern is documented in the Radix UI composition guide
About this share
Contributormblode
Repositorymblode/shares
CreatedFeb 9, 2026
Environmentreact
View on GitHub