Tooltip
A short text label that appears on hover and keyboard focus to name an icon-only control or reveal its shortcut.
import { PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export default function TooltipDemo() {
return (
<Tooltip>
<TooltipTrigger
render={
<Button variant="outline" size="icon" aria-label="Add to library" />
}
>
<PlusIcon />
</TooltipTrigger>
<TooltipContent>Add to library</TooltipContent>
</Tooltip>
)
}Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/tooltip.jsonWrap your app once in TooltipProvider. It gives every tooltip the same delay and lets neighbors open instantly once one is showing — hover along a toolbar and the labels follow without waiting.
import { TooltipProvider } from "@/components/ui/tooltip"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<TooltipProvider delay={400}>{children}</TooltipProvider>
</body>
</html>
)
}Usage
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"<Tooltip>
<TooltipTrigger render={<Button variant="outline" size="icon" aria-label="Add to library" />}>
<PlusIcon />
</TooltipTrigger>
<TooltipContent>Add to library</TooltipContent>
</Tooltip>prfct tooltips are inverted — text in the background color on a foreground fill — so they read as a label on top of any surface in either mode, and enter faster than other popups (160ms) because they appear dozens of times per session.
Examples
Keyboard shortcuts
Tooltips are the natural place to teach shortcuts. Put a Kbd after the label; it restyles itself for the inverted surface. Hover along the toolbar: after the first tooltip opens, the next ones appear immediately.
import {
BoldIcon,
CodeIcon,
ItalicIcon,
LinkIcon,
StrikethroughIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { Separator } from "@/components/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const actions = [
{ label: "Bold", icon: BoldIcon, keys: ["⌘", "B"] },
{ label: "Italic", icon: ItalicIcon, keys: ["⌘", "I"] },
{ label: "Strikethrough", icon: StrikethroughIcon, keys: ["⌘", "⇧", "X"] },
{ label: "Inline code", icon: CodeIcon, keys: ["⌘", "E"] },
{ label: "Link", icon: LinkIcon, keys: ["⌘", "K"] },
]
export default function TooltipShortcuts() {
return (
<div
role="toolbar"
aria-label="Formatting"
className="flex items-center gap-0.5 rounded-xl border bg-card p-1 shadow-raised"
>
{actions.map(({ label, icon: Icon, keys }, index) => (
<div key={label} className="contents">
{index === 3 && (
<Separator
orientation="vertical"
className="mx-1 h-5! self-center!"
/>
)}
<Tooltip>
<TooltipTrigger
render={
<Button variant="ghost" size="icon-sm" aria-label={label} />
}
>
<Icon />
</TooltipTrigger>
<TooltipContent>
{label}
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
</TooltipContent>
</Tooltip>
</div>
))}
</div>
)
}Side
side places the tooltip on the top (default), right, bottom or left of its trigger. It flips automatically near the viewport edges.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const sides = ["top", "right", "bottom", "left"] as const
export default function TooltipSides() {
return (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{sides.map((side) => (
<Tooltip key={side}>
<TooltipTrigger
render={<Button variant="outline" className="capitalize" />}
>
{side}
</TooltipTrigger>
<TooltipContent side={side}>Shown on the {side}</TooltipContent>
</Tooltip>
))}
</div>
)
}Disabled triggers
A disabled button receives no pointer or focus events, so a tooltip on it could never open. Keep it reachable with focusableWhenDisabled and use the tooltip to explain what's missing.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export default function TooltipDisabled() {
return (
<Tooltip>
{/*
A disabled <button> receives no pointer or focus events, so its tooltip
could never open. `focusableWhenDisabled` keeps it reachable while
still blocking activation.
*/}
<TooltipTrigger render={<Button disabled focusableWhenDisabled />}>
Publish
</TooltipTrigger>
<TooltipContent>Add a title before publishing</TooltipContent>
</Tooltip>
)
}Delay
The provider sets a 400ms delay for every tooltip. Override it per trigger with delay — shorter for dense tools people scan quickly, longer where accidental tooltips would get in the way.
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const delays = [
{ label: "Instant", delay: 0 },
{ label: "Default", delay: undefined },
{ label: "Patient", delay: 1000 },
]
export default function TooltipDelay() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
{delays.map(({ label, delay }) => (
<Tooltip key={label}>
<TooltipTrigger delay={delay} render={<Button variant="outline" />}>
{label}
</TooltipTrigger>
<TooltipContent>
{delay === undefined
? "Uses the provider’s 400ms"
: `Opens after ${delay}ms`}
</TooltipContent>
</Tooltip>
))}
</div>
)
}Guidelines
When to use
- To name icon-only buttons. Every icon button needs an
aria-label; the tooltip shows the same text to sighted pointer users. - To reveal keyboard shortcuts for common actions.
- To expand truncated text, such as a long file name.
When not to use
- For information people need to complete a task — show it on the page. Tooltips are invisible until someone thinks to look.
- For interactive content: links, buttons, inputs. Tooltips disappear when the pointer leaves — use a Popover.
- For rich previews — use a Hover Card.
- On touch-only interfaces, where there is no hover — make the label visible instead.
Write labels, not sentences
A tooltip is read in a glance: two to four words, sentence case, no trailing period. Describe the action, not the icon.
Accessibility
A tooltip is a visual hint for sighted pointer and keyboard users. It opens on hover and on keyboard focus and never takes focus itself — but Base UI doesn't wire it to the trigger with aria-describedby, so screen readers don't announce it. The trigger's own accessible name has to carry the meaning.
| Key | Behavior |
|---|---|
Tab | Focusing the trigger opens the tooltip after the delay. |
Esc | Closes the tooltip without moving focus. |
- Put the label in the accessible name. Give icon-only triggers an
aria-labelwith the same text as the tooltip. A tooltip supplements the name; it never replaces it. - Hoverable. Moving the pointer onto the tooltip keeps it open, so people using screen magnification can read it (WCAG 1.4.13). Set
disableHoverablePopuponly for tooltips that follow the cursor. - Dismissible. Esc hides it without moving the pointer or focus.
- Plain text only. Content inside a tooltip isn't reachable with the keyboard; never put links or buttons in it.
API reference
TooltipProvider
Shares a delay between tooltips. Doesn't render an element.
Tooltip
The root. Doesn't render an element. Accepts every prop of Base UI Tooltip.Root.
TooltipTrigger
The element that shows the tooltip. Renders a <button>; use render to render a prfct Button.
TooltipContent
Renders the portal, positioner and inverted popup with an arrow.