Overview
<Tooltip /> is the canonical hover/focus hint primitive. Wrap any single trigger element to
attach a small floating message that opens on hover and on keyboard focus.
Overview — tooltips at top, right, bottom, and left
Tooltip is intentionally read-only — its surface has pointer-events: none and never
receives focus. For interactive overlays (rich content, links, buttons-inside-the-floater),
reach for <Popover /> instead.
When to use
- Quick clarification for an icon-only button:
<Tooltip content="Save changes"><Button leftIcon={…} aria-label="Save" /></Tooltip>. - Disambiguating a truncated label or a status indicator.
- Surfacing a keyboard shortcut:
<Tooltip content={<>Save · <Kbd>⌘S</Kbd></>}>…</Tooltip>. - Showing extra metadata (timestamps, IDs) without crowding the visible UI.
When NOT to use
- The content is interactive — use
<Popover />. - The information is critical — Tooltips are skipped on touch devices and read by screen readers
via
aria-describedby(associative, not announced as a live region). Critical info belongs in the visible UI. - The trigger is wrapped twice. Tooltip clones a single child; multi-element triggers must
wrap in a
<span>first.
Anatomy
[ trigger ] ──hover/focus──▶ ┌──────────────────┐
│ content + arrow │
└──────────────────┘[ trigger ] ──hover/focus──▶ ┌──────────────────┐
│ content + arrow │
└──────────────────┘- trigger — your single React element. Tooltip clones it to attach
ref,aria-describedby, and pointer / focus handlers. - content — the floating surface. Carries
role="tooltip"and a generatedidthat the trigger references viaaria-describedbywhile open. - arrow — optional SVG arrow that points back at the trigger after Floating UI's
flip()resolves. Toggle withshowArrow={false}.
Variants
Pick by how the tooltip should integrate with the surface beneath it:
solid— opaque fill + contrast text. Default. The conventional tooltip look.outline— paper background, 1px colored border + colored text. Brand-aligned hints (link previews, primary actions).soft— subtle tinted background + colored text + low-opacity colored border. Editorial / settings-pane tooltips; calmer thansolid.inverted— platform-default light-on-dark / dark-on-light. Ignorescolor— the inverted variant has a fixed dark/light contrast palette so it works on every brand.
The 4 variants × 7 colors compound matrix mirrors Badge's shape exactly.
Sizes
| Size | Padding | Font | Max-width | Arrow size |
|---|---|---|---|---|
sm | px-2 py-1 | text-xs | max-w-xs | 12×6 |
md | px-2.5 py-1.5 | text-sm | max-w-sm | 16×8 |
lg | px-3 py-2 | text-base | max-w-md | 20×10 |
Placement
12 placements — top / right / bottom / left × start / center / end. Floating UI's
flip middleware automatically swaps to the opposite side at viewport edges, and the start /
end alignment uses logical properties so RTL falls out automatically: top-start is
top-left in LTR and top-right in RTL.
The post-flip placement is reflected on the floating element via data-placement so consumers
who care about animation direction or arrow rotation can branch on it.
Delay state
Two timers control hover behaviour:
openDelay(default400ms) — filters out accidental sweeps across the UI. Matches MUI / Radix UX research findings on attention thresholds.closeDelay(default150ms) — gives users time to move the cursor onto the floating surface. Hovering the surface itself cancels the close timer, so users can move from trigger → tooltip without it disappearing.
Set both to 0 for instant tooltips; set high values (~1000ms+) for "patient" tooltips that
only appear on deliberate hovers.
Accessibility
- Floating surface carries
role="tooltip"and a generatedid. The trigger getsaria-describedby="<id>"while open — exactly the ARIA Tooltip pattern. - Existing
aria-describedbyvalues on the trigger are preserved (the new id is appended). - Tooltip opens on pointerenter (mouse, pen, touch on platforms that emit it) and focus
(keyboard). It closes on pointerleave, blur, and
Escape. - The escape stack ensures only the topmost tooltip closes on
Escape, not all open overlays. - Tooltip never receives focus itself (
pointer-events-none+ notabIndex) — content is not interactive. For interactive overlays use<Popover />. - The arrow carries
aria-hidden="true". disabled={true}skips all opening logic; the trigger renders normally.- axe-core: zero violations across every
variant × colorcell.
Examples
Basic
Variants
Sizes
Colors
Placements
Delay
WithoutArrow
LongContent
Disabled
Controlled
PortalContainer
Theming
Theme-level overrides flow through useThemedClasses exactly like every other DS component:
defineTheme({
components: {
Tooltip: {
defaultProps: { variant: 'inverted', size: 'sm', openDelay: 200 },
styleOverrides: {
content: 'shadow-lg',
arrow: '',
},
},
},
});defineTheme({
components: {
Tooltip: {
defaultProps: { variant: 'inverted', size: 'sm', openDelay: 200 },
styleOverrides: {
content: 'shadow-lg',
arrow: '',
},
},
},
});Per-instance overrides via className / sx / style are merged on top of the theme overrides,
which are merged on top of the recipe — same precedence as everywhere else in the DS.