Tooltip
A shadcn Base UI tooltip with the Depth surface of the buttons that glides between neighbouring triggers.
Hover one button, then slide along the toolbar. The tooltip glides over to the next button and resizes to its label instead of closing and reopening.
import {
BoldIcon,
CodeIcon,
ItalicIcon,
LinkIcon,
ListIcon,
QuoteIcon,
UnderlineIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const TOOLTIP_OPEN_DELAY_MS = 400;
const ACTIONS = [
{ icon: BoldIcon, keys: ["⌘", "B"], label: "Bold" },
{ icon: ItalicIcon, keys: ["⌘", "I"], label: "Italic" },
{ icon: UnderlineIcon, keys: ["⌘", "U"], label: "Underline" },
{ icon: LinkIcon, keys: ["⌘", "K"], label: "Add link" },
{ icon: ListIcon, keys: [], label: "Bulleted list" },
{ icon: QuoteIcon, keys: [], label: "Quote" },
{ icon: CodeIcon, keys: ["⌘", "E"], label: "Inline code" },
] as const;
export default function TooltipToolbarExample() {
return (
<div className="flex justify-center p-10">
<TooltipProvider delay={TOOLTIP_OPEN_DELAY_MS}>
<div
aria-label="Formatting"
className="border-border bg-background flex items-center gap-0.5 rounded-xl border p-1 shadow-xs"
role="toolbar"
>
{ACTIONS.map(({ icon: Icon, keys, label }) => (
<Tooltip key={label}>
<TooltipTrigger
render={
<Button aria-label={label} size="icon-sm" variant="ghost" />
}
>
<Icon />
</TooltipTrigger>
<TooltipContent>
{label}
{keys.length > 0 ? (
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
) : null}
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
</div>
);
}
Installation
$ bunx --bun shadcn@latest add @notra/notra-tooltipThis replaces components/ui/tooltip.tsx. The surface is built from your --foreground and --background tokens, so it inverts with your theme like the secondary button.
Moving between tooltips
Wrap neighbouring triggers in one TooltipProvider. Every tooltip inside it shares a single popup, so moving to the next trigger slides the popup over, animates its width and swaps the label. The delay only applies to the first tooltip. Tooltips outside a provider open and close on their own.
<TooltipProvider delay={400}>
<Tooltip>
<TooltipTrigger render={<Button size="icon-sm" variant="ghost" />}>
<BoldIcon />
</TooltipTrigger>
<TooltipContent>
Bold
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>B</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</TooltipProvider>
Sides
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const SIDES = ["top", "right", "bottom", "left"] as const;
export default function TooltipSidesExample() {
return (
<div className="flex flex-wrap items-center justify-center gap-3 p-12">
<TooltipProvider>
{SIDES.map((side) => (
<Tooltip key={side}>
<TooltipTrigger
render={<Button className="capitalize" variant="outline" />}
>
{side}
</TooltipTrigger>
<TooltipContent side={side}>Opens on the {side}</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
);
}
The popup scales in from its trigger and drifts a couple of pixels away from it. With reduced motion it only fades.