How to use a shadcn <Button> inside shadcn <TooltipProvider> ?
Answered
European imported fire ant posted this in #help-forum
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
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:
This is the button code in a component called "ClientChatLogButton". It displays like an icon (<MessagesSquare /> from Lucide)
This is the Tooltip.
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 ?
<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 ?
![Image](https://cdn.discordapp.com/attachments/1205923408697626654/1205923409108934746/image.png?ex=65da22b1&is=65c7adb1&hm=5122c663c609e21aed7fb309acad094662f8e48ee55394b13f852008e3bba8a4&)
Answered by European imported fire ant
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
48 Replies
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
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)
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
Im not sure I understand why thats an error so I didnt want to just suppress it
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
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.
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
And why wouldnt it in my case ?
the button is a client component btw
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
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
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
"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
isnt that serious though ? Should I suppress it ?
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
is the button file, client as well.
You can, if its just a client side button.
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
Button comes from shadcn
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
in this file
import { Button } from "../ui/button"
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
ctrl + click the link in your IDE
does the file in the root of that say 'use client;?
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
its not. I dont think shadcn gives you client components
Ah I see now
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
you can make it client
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
its a server thing inside a client component
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
So, answer me this. Does the button serve any server functionality at all?
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
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
![Avatar](https://cdn.discordapp.com/avatars/209129015154311171/a_a38e73816872a7950286c2727aad749c.gif?size=256)
Smultar.json
Could you open the inspect element in your browser
It tells you, what extactly is out of sync.
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
![Image](https://cdn.discordapp.com/attachments/1205923408697626654/1205942790949834862/image.png?ex=65da34be&is=65c7bfbe&hm=1490a3d4795550762db2456aef9754e1350d54eaf25b1716ba4cbb219d1c8fc3&)
Also
![Image](https://cdn.discordapp.com/attachments/1205923408697626654/1205942904812609657/image.png?ex=65da34d9&is=65c7bfd9&hm=9f84fcce4ba9d66747f45723ce576d09e83532db6816e3743b927b3f1737de42&)
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
I think the problem is that the icon from Lucide is a button, and its already in a button
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
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
![Avatar](https://cdn.discordapp.com/embed/avatars/5.png)
Large Münsterländer
bro just set the "asChild" prop on <TooltipTrigger> lmfaoo
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
what does this do ?
![Avatar](https://cdn.discordapp.com/embed/avatars/5.png)
Large Münsterländer
It IS what you are searching for
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
Yea im just asking what asChild does
![Avatar](https://cdn.discordapp.com/avatars/236985086627676160/a2a37786dba55fdb502e68350c688972.webp?size=256)
rortan
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>
![Avatar](https://cdn.discordapp.com/embed/avatars/5.png)
Large Münsterländer
Take it or leave it
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
Not very helpful today are we
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
So would you advise I go with the asChild or is my solution ok too ?
![Avatar](https://cdn.discordapp.com/avatars/236985086627676160/a2a37786dba55fdb502e68350c688972.webp?size=256)
rortan
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
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
European imported fire antOP
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 ?