Tooltip
A small label on hover or focus, with a delay that respects intent.
$
pnpm dlx shadcn@latest add https://lodeui.com/r/tooltip.jsoncomponents/examples/tooltip-demo.tsx
"use client";
import { Bold, Italic, Link2, Underline } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip } from "@/components/ui/tooltip";
const tools = [
{ icon: <Bold />, label: "Bold", keys: "⌘B" },
{ icon: <Italic />, label: "Italic", keys: "⌘I" },
{ icon: <Underline />, label: "Underline", keys: "⌘U" },
{ icon: <Link2 />, label: "Add link", keys: "⌘K" },
];
export default function TooltipDemo() {
return (
<div className="flex gap-1 rounded-lg border border-border bg-card p-1">
{tools.map((t) => (
<Tooltip key={t.label} content={<span className="flex gap-2">{t.label}<span className="opacity-60">{t.keys}</span></span>}>
<Button size="icon-sm" variant="ghost" aria-label={t.label}>{t.icon}</Button>
</Tooltip>
))}
</div>
);
}When to use
- Naming icon-only buttons and showing their shortcuts.
When not to use
- Essential information: put it in the UI.
- Interactive content: use a popover.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/tooltip.json1Install the dependencies
npm install motion2Copy the source into your project
components/ui/tooltip.tsx"use client"; import * as React from "react"; import { AnimatePresence, motion } from "motion/react"; import { cn } from "@/lib/utils"; export interface TooltipProps { content: React.ReactNode; children: React.ReactElement<React.HTMLAttributes<HTMLElement>>; side?: "top" | "bottom"; delay?: number; className?: string; } /** Shows on hover after a short delay, and immediately on keyboard focus. */ export function Tooltip({ content, children, side = "top", delay = 350, className }: TooltipProps) { const id = React.useId(); const [open, setOpen] = React.useState(false); const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null); const show = (wait: number) => { if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => setOpen(true), wait); }; const hide = () => { if (timer.current) clearTimeout(timer.current); setOpen(false); }; React.useEffect(() => () => hide(), []); const trigger = React.cloneElement(children, { "aria-describedby": open ? id : undefined, onPointerEnter: () => show(delay), onPointerLeave: hide, onFocus: () => show(0), onBlur: hide, onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => { if (e.key === "Escape") hide(); children.props.onKeyDown?.(e); }, } as React.HTMLAttributes<HTMLElement>); return ( <span className="relative inline-flex"> {trigger} <AnimatePresence> {open && ( <motion.span id={id} role="tooltip" initial={{ opacity: 0, scale: 0.92, y: side === "top" ? 4 : -4 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.08 } }} transition={{ type: "spring", stiffness: 600, damping: 32 }} style={{ transformOrigin: side === "top" ? "bottom center" : "top center" }} className={cn( "pointer-events-none absolute left-1/2 z-50 -translate-x-1/2 whitespace-nowrap rounded-md bg-primary px-2.5 py-1.5 text-xs font-medium text-primary-foreground shadow-lg", side === "top" ? "bottom-full mb-2" : "top-full mt-2", className, )} > {content} </motion.span> )} </AnimatePresence> </span> ); }
Usage
Import it from @/components/ui/tooltip after installing.
example.tsx
import { Bold } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip } from "@/components/ui/tooltip";
export function BoldButton() {
return (
<Tooltip content="Bold ⌘B">
<Button size="icon" variant="ghost" aria-label="Bold"><Bold /></Button>
</Tooltip>
);
}API reference
Tooltip
Wraps one element; shows on hover after a delay and immediately on keyboard focus.
| Prop | Type | Default | Description |
|---|---|---|---|
| content | ReactNode | – | Tooltip text. |
| children | ReactElement | – | The trigger; handlers and aria-describedby are added to it. |
| side | "top" | "bottom" | "top" | Placement. |
| delay | number | 350 | Hover delay in ms. |
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Focus shows the tooltip |
| Esc | Hide it |
Accessibility
- role="tooltip" linked to the trigger with aria-describedby.
- The trigger still needs its own aria-label; the tooltip describes, it doesn't name.
Motion
- Scales in from the side it points to (stiffness 600).
Responsive behavior
- Touch devices don't hover, so don't hide anything important in a tooltip.
Performance
- Rendered only while open.
Notes for AI
- Never wrap disabled buttons; they don't fire pointer events.
This page as Markdown, for assistants and coding agents:
/components/tooltip/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt