Next.js Discord

Discord Forum

How to use a shadcn <Button> inside shadcn <TooltipProvider> ?

Answered
Ichneumonid wasp posted this in #help-forum
Open in Discord
Ichneumonid waspOP
Hello, I am trying to create a tooltip on a Button. Both are taken from shadcn. The way I have it set up is the following:
<Button
    variant='ghost'
    onClick={() => { router.push("/") }}
>
    <MessagesSquare />
</Button>

This is the button code in a component called "ClientChatLogButton". It displays like an icon (<MessagesSquare /> from Lucide)

This is the Tooltip.
<TooltipProvider>
    <Tooltip>
        <TooltipTrigger>
            <ClientChatLogButton />
        </TooltipTrigger>
        <TooltipContent>
            <p>View saved</p>
        </TooltipContent>
    </Tooltip>
</TooltipProvider>

When the page loads, I get hydration errors but everything works. Below are the errors. From my understanding it doesnt like the fact that there is a button inside the TooltipProvider. When I comment out the <ClientChatLogButton /> component and replace it with plain text it wont complain. Any Ideas on how I can implement this functionality ?
Answered by Ichneumonid wasp
Yep, that was the issue, I changed my custom button like this and it worked
"use client"

import { MessagesSquare } from "lucide-react"
import { useRouter } from "next/navigation"

function ClientChatLogButton() {
    const router = useRouter()

    return (
        <div
            className='
            onClick={() => { router.push("/chats") }}
        >
            <MessagesSquare />
        </div>
    )
}

export default ClientChatLogButton
View full answer

48 Replies

Have you tried adding the suppressHydrationWarning attribute to the tooltip

<TooltipProvider>
    <Tooltip suppressHydrationWarning > # See my change here
        <TooltipTrigger>
            <ClientChatLogButton />
        </TooltipTrigger>
        <TooltipContent>
            <p>View saved</p>
        </TooltipContent>
    </Tooltip>
</TooltipProvider>
This is [solution #3 from hydration errors](https://nextjs.org/docs/messages/react-hydration-error)
Ichneumonid waspOP
Im not sure I understand why thats an error so I didnt want to just suppress it
So when react renders things on the server. It does a check to make sure what your server sees, is the same thing the client sees.
Ichneumonid waspOP
And why wouldnt it in my case ?
the button is a client component btw
I dont know the source code for the button, but sometimes when components are generated, the CSS classes have procedrually generated classes
So, if you use a math.random function for example
Your server would get 5 and your client would get 2
Ichneumonid waspOP
"use client"

import { Button } from "../ui/button"
import { MessagesSquare } from "lucide-react"
import { useRouter } from "next/navigation"

function ClientChatLogButton() {
    const router = useRouter()

    return (
        <Button
            variant='ghost'
            onClick={() => { router.push("/chats") }}
        >
            <MessagesSquare />
        </Button>
    )
}

export default ClientChatLogButton
this is the button
@Smultar.json Your server would get 5 and your client would get 2
Ichneumonid waspOP
isnt that serious though ? Should I suppress it ?
is the button file, client as well.
@Ichneumonid wasp isnt that serious though ? Should I suppress it ?
You can, if its just a client side button.
Ichneumonid waspOP
Button comes from shadcn
in this file
import { Button } from "../ui/button"
Ichneumonid waspOP
ctrl + click the link in your IDE
does the file in the root of that say 'use client;?
Ichneumonid waspOP
its not. I dont think shadcn gives you client components
Ah I see now
you can make it client
Ichneumonid waspOP
its a server thing inside a client component
So, answer me this. Does the button serve any server functionality at all?
Ichneumonid waspOP
import { Button } from "../ui/button"

This is used as a base to make my own buttons. Some of them are client, others are server side
Even though I made it client I still get the same error
Could you open the inspect element in your browser
It tells you, what extactly is out of sync.
Ichneumonid waspOP
Also
Ichneumonid waspOP
I think the problem is that the icon from Lucide is a button, and its already in a button
Ichneumonid waspOP
Yep, that was the issue, I changed my custom button like this and it worked
"use client"

import { MessagesSquare } from "lucide-react"
import { useRouter } from "next/navigation"

function ClientChatLogButton() {
    const router = useRouter()

    return (
        <div
            className='
            onClick={() => { router.push("/chats") }}
        >
            <MessagesSquare />
        </div>
    )
}

export default ClientChatLogButton
Answer
Large Münsterländer
bro just set the "asChild" prop on <TooltipTrigger> lmfaoo
@Large Münsterländer bro just set the "asChild" prop on <TooltipTrigger> lmfaoo
Ichneumonid waspOP
what does this do ?
Large Münsterländer
It IS what you are searching for
Ichneumonid waspOP
Yea im just asking what asChild does
@Ichneumonid wasp Yea im just asking what asChild does
It merges the parent's components props down with it's child
both Tooltip and Button render a <button> tag, if you add aschild to the tooltip component it'll merge the Tooltip event listeners to the Button component and render both as a single tag
works in server components and it's a really nice composition practice although the radix team themselves said it has drawbacks
It requires that the component only has a single child though. e.g. this wont work
<TooltipTrigger asChild>
  <Button />
  <span />
</TooltipTrigger>
but this will if you install the @radix-ui/react-slot lib (which is the lib they use internally to create the asChild prop)
<TooltipTrigger asChild>
  <Slottable>
      <Button />
  </Slottable>
  <span />
</TooltipTrigger>
@Ichneumonid wasp Yea im just asking what asChild does
Large Münsterländer
Take it or leave it
Ichneumonid waspOP
Not very helpful today are we
@rortan It requires that the component only has a single child though. e.g. this wont work <TooltipTrigger asChild> <Button /> <span /> </TooltipTrigger>
Ichneumonid waspOP
So would you advise I go with the asChild or is my solution ok too ?
@Ichneumonid wasp So would you advise I go with the asChild or is my solution ok too ?
from what I can see you made a div interactive, that's not an ok solution not even in regular html
Do this
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild><ClientChatLogButton /></TooltipTrigger>
  </Tooltip>
  <TooltipContent>
            <p>View saved</p>
        </TooltipContent>
<TooltipProvider/>
make sure to use React.forwardRef on your ClientChatLogButton comp
@rortan Do this tsx <TooltipProvider> <Tooltip> <TooltipTrigger asChild><ClientChatLogButton /></TooltipTrigger> </Tooltip> <TooltipContent> <p>View saved</p> </TooltipContent> <TooltipProvider/>
Ichneumonid waspOP
So I use asChild on tooltiptrigger, then in my component I receive that as a forwardref and render it as the button I already have in my component right ?