ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: Hydration failed in Next.js SSR when using shadcn/ui Button (asChild/Slot?) leading to client re-render

Open yurirxmos opened this issue 1 month ago • 2 comments

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

yurirxmos avatar Dec 01 '25 11:12 yurirxmos

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

priyanshuKumar56 avatar Dec 01 '25 16:12 priyanshuKumar56

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>

speedhawk21 avatar Dec 14 '25 03:12 speedhawk21