[bug]: Hydration failed in Next.js SSR when using shadcn/ui Button (asChild/Slot?) leading to client re-render
Describe the bug
Im getting a React hydration error in Next.js. The error points to shadcn/ui Button component, specifically around the rendered Comp element. During hydration, the HTML from server does not match the first client render, so React discards the SSR tree and regenerates it on the client.
Affected component/components
Button
How to reproduce
Steps to reproduce 1. Create a Next.js app with app router and SSR enabled. 2. Install shadcn/ui and add Button component. 3. Use Button in a server-rendered page with the code below. 4. Run next dev and open the page. 5. Observe hydration warning and client re-render.
Codesandbox/StackBlitz link
No response
Logs
## Error Type
Console Error
## Error Message
In HTML, <button> cannot be a descendant of <button>.
This will cause a hydration error.
...
<TooltipProviderProvider scope={undefined} isOpenDelayedRef={{current:true}} delayDuration={0} ...>
<Tooltip data-slot="tooltip">
<Popper __scopePopper={{Popper:[...]}}>
<PopperProvider scope={{Popper:[...]}} anchor={null} onAnchorChange={function bound dispatchSetState}>
<TooltipProvider scope={undefined} contentId="radix-_R_a..." open={false} stateAttribute="closed" ...>
<TooltipTrigger>
<TooltipTrigger data-slot="tooltip-tr...">
<PopperAnchor asChild={true} __scopePopper={{Popper:[...]}}>
<Primitive.div asChild={true} ref={function}>
<Primitive.div.Slot ref={function}>
<Primitive.div.SlotClone ref={function}>
<Primitive.button aria-describedby={undefined} data-state="closed" data-slot="tooltip-tr..." ...>
> <button
> aria-describedby={undefined}
> data-state="closed"
> data-slot="tooltip-trigger"
> onPointerMove={function handleEvent}
> onPointerLeave={function handleEvent}
> onPointerDown={function handleEvent}
> onFocus={function handleEvent}
> onBlur={function handleEvent}
> onClick={function handleEvent}
> ref={function}
> >
<Button variant="ghost" size="icon" className="" onClick={function onClick}>
> <button
> data-slot="button"
> className={"hover:cursor-pointer inline-flex items-center justify-center gap-2 whit..."}
> onClick={function onClick}
> >
...
at button (<anonymous>:null:null)
at Button (components/ui/button.tsx:52:5)
at Navbar (components/navbar.tsx:89:15)
at Header (components/layout-controller/header-controller.tsx:17:7)
at RootLayout (app/layout.tsx:120:15)
## Code Frame
50 |
51 | return (
> 52 | <Comp
| ^
53 | data-slot="button"
54 | className={cn(buttonVariants({ variant, size, className }))}
55 | {...props}
Next.js version: 16.0.5 (Turbopack)
System Info
Environment
• Next.js: 16
• React: 18.x
• shadcn/ui version/commit: latest
• Tailwind: 4
• Node: 19
• Browser: Chrome/Firefox version
• OS: Windows/Linux/macOS
Before submitting
- [x] I've made research efforts and searched the documentation
- [x] I've searched for existing issues
Potential Fix Recommendation
This hydration error is caused by nested
Problem Summary
<TooltipTrigger> wraps its children in a
How It Can Be Fixed (Project Code Change)
You should update the documentation or internal component usage to instruct users to include asChild when using shadcn Button inside Radix triggers:
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={...}>
...
</Button>
</TooltipTrigger>
This prevents Radix from wrapping the Button in its own
Still getting hydration errors, even with asChild.
components/public/PublicNavbar.tsx (65:13) @ PublicNavbar
63 | <DropdownMenu>
64 | <DropdownMenuTrigger asChild className="min-[696px]:hidden flex">
> 65 | <Button variant="ghost" size="icon">
| ^
66 | <Menu className="h-5 w-5" />
67 | </Button>
68 | </DropdownMenuTrigger>