Tooltip
Contextual labels that appear on hover and focus. Portaled to document.body, auto-flips when near viewport edges, and dismisses instantly on mouse-out.
Preview
Placement
Four sides, each with automatic flip detection. If the preferred side would overflow the viewport the tooltip moves to the opposite side. Horizontal position is always clamped within the safe area.
Behavior
| Trigger | Action | Default delay |
|---|---|---|
mouseenter | Show after delay | 400ms |
mouseleave | Hide immediately | — |
focus | Show after delay | 400ms |
blur | Hide immediately | — |
Implementation
The tooltip panel is portaled to document.body so it is never clipped by overflow: hidden ancestors. Position is calculated after mount using getBoundingClientRect.
import { Tooltip } from "@/components/ui/Tooltip";
import { Button } from "@/components/ui/Button";
// Basic usage
<Tooltip content="Copy to clipboard">
<Button variant="ghost" size="icon" aria-label="Copy">
<Copy className="w-4 h-4" />
</Button>
</Tooltip>
// Placement — flips automatically if it overflows
<Tooltip content="Top tooltip" side="top"> <Button>Top</Button> </Tooltip>
<Tooltip content="Bottom tooltip" side="bottom"> <Button>Bottom</Button> </Tooltip>
<Tooltip content="Left tooltip" side="left"> <Button>Left</Button> </Tooltip>
<Tooltip content="Right tooltip" side="right"> <Button>Right</Button> </Tooltip>
// Rich content
<Tooltip content={
<span>
Press <kbd className="font-mono">⌘K</kbd> to open
</span>
}>
<Button variant="ghost">Search</Button>
</Tooltip>
// Custom delay
<Tooltip content="Appears after 100ms" delay={100}>
<Button variant="secondary">Fast</Button>
</Tooltip>Props
| Prop | Type | Default | Description |
|---|---|---|---|
content | ReactNode | — | Tooltip label. Accepts strings or any React node. |
side | "top" | "bottom" | "left" | "right" | "top" | Preferred placement. Flips to the opposite side if it would overflow the viewport. |
delay | number | 400 | Milliseconds before the tooltip appears on hover/focus. |
children | ReactElement | — | The trigger element. Must be a single React element — Tooltip clones it to attach event handlers. |
Mobile
Tooltips depend on hover — a concept that doesn't exist on touch screens. On mobile, tooltips are invisible unless you provide an alternative trigger strategy.
Article
Long press for tooltip
On mobile: tap-and-hold or use visible labels instead of hover tooltips
| Scenario | Guidance |
|---|---|
| No hover on touch | Tooltips do not appear on tap by default. Never put information in a tooltip that is required to understand or operate the control — it will be inaccessible to touch users. |
| Tap-to-show pattern | For icon-only controls, show the tooltip on the first tap and perform the action on the second tap. Or show a bottom drawer with the action label on long-press. |
| Prefer visible labels | On mobile, replace icon-only buttons that rely on tooltips with visible text labels, or show labels beneath icons. This removes the need for tooltip discovery entirely. |
| Long-press trigger | Add a touchstart timer (300ms) to trigger the tooltip on long-press. Dismiss it on touchend or after 2 seconds. This mirrors iOS UIMenu behavior. |
| Positioning on small screens | Tooltips positioned to the top can be clipped by the top of the viewport on short-screen devices. Prefer bottom or auto placement on mobile. |
Accessibility
- →The tooltip panel has role="tooltip". Wire aria-describedby on the trigger pointing to the tooltip id for full ARIA compliance.
- →Tooltips appear on focus as well as hover — keyboard users get the same information as mouse users.
- →Tooltip text must be purely supplementary. Never put information in a tooltip that is required to operate the control.
- →Avoid rich interactive content inside tooltips — they are pointer-events-none and cannot be focused.
- →For icon-only buttons, prefer aria-label over a tooltip as the primary accessible name. The tooltip then provides a secondary affordance.