Use Tooltip for brief extra context, not essential instructions or interactive content. Keep TooltipProvider around the relevant controls. A tooltip is not a substitute for its trigger's accessible name.
Live preview
import { ShieldCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
export default function TooltipDemo() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<Button variant="noShadow" size="icon" aria-label="Registry verification status" />
}
>
<ShieldCheck aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>
<p>Verified in fresh Next.js and Vite projects.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/tooltip.jsonnpm install @base-ui/react@^1.8.0Create components/ui/tooltip.tsx:
"use client";
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { cn } from "@/lib/utils";
type TooltipContentProps = TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
| "align"
| "alignOffset"
| "arrowPadding"
| "collisionAvoidance"
| "collisionBoundary"
| "collisionPadding"
| "disableAnchorTracking"
| "positionMethod"
| "side"
| "sideOffset"
| "sticky"
>;
function TooltipProvider({ delay = 0, timeout = 300, ...props }: TooltipPrimitive.Provider.Props) {
return <TooltipPrimitive.Provider delay={delay} timeout={timeout} {...props} />;
}
function Tooltip(props: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root {...props} />;
}
function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
align = "center",
alignOffset = 0,
arrowPadding = 0,
children,
className,
collisionAvoidance,
collisionBoundary,
collisionPadding = 0,
disableAnchorTracking,
positionMethod = "fixed",
side = "top",
sideOffset = 4,
sticky,
...props
}: TooltipContentProps) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
arrowPadding={arrowPadding}
collisionAvoidance={collisionAvoidance}
collisionBoundary={collisionBoundary}
collisionPadding={collisionPadding}
disableAnchorTracking={disableAnchorTracking}
positionMethod={positionMethod}
side={side}
sideOffset={sideOffset}
sticky={sticky}
className="isolate z-50"
>
<TooltipPrimitive.Popup
role="tooltip"
data-slot="tooltip-content"
{...props}
className={(state) =>
cn(
"z-50 origin-(--transform-origin) overflow-hidden rounded-base border-2 border-border bg-main px-3 py-1.5 text-sm font-base text-main-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
The trigger example additionally uses Button.
#Usage
import { Info } from "lucide-react";
import { Button } from "@/components/ui/button";
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
export default function InformationHint() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<Button size="icon" variant="outline" aria-label="Workspace information" />}
>
<Info aria-hidden="true" />
</TooltipTrigger>
<TooltipContent>Visible to workspace members only.</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
TooltipProvider.delay | Opening delay in milliseconds | 0 through this wrapper |
Tooltip.open / onOpenChange | Controlled visibility | Uncontrolled |
Tooltip.defaultOpen | Initial visibility | false |
TooltipTrigger.render | Focusable-control composition | Primitive trigger |
TooltipContent.side / align | Preferred placement | top / center |
TooltipContent.sideOffset | Trigger gap | 4 |
TooltipProvider.timeout | Time to skip the opening delay between tooltips, in milliseconds | 300 |
TooltipContent.alignOffset / arrowPadding / collisionPadding | Alignment, arrow, and collision-boundary spacing, in pixels | 0 |
TooltipContent.positionMethod | CSS positioning method | fixed |
#Accessibility
Give icon-only controls their own aria-label and hide decorative icons. Keep tooltip text concise. Do not put buttons, links, or required form help inside a tooltip. Touch users may not encounter it, so controls must remain understandable without hover content.