Tooltip
Short hover and keyboard-focus hint anchored to a control. Composable trigger and content on Base UI, role=tooltip, 4 sides × 3 alignments with collision flipping, 5 intent plates, 3 sizes, an optional arrow, grouped delays via a provider, and a label/description ARIA switch.
pnpm add @tessinaui/uiA tooltip never appears on touch. Base UI opens it on hover with
mouseOnly, and on focus only under keyboard modality — so a phone or tablet
user sees nothing, and there is no long-press fallback. Anything a touch user
must be able to read cannot live in a tooltip: put it in visible text, in the
control's aria-label, or behind a Popover, which
opens on tap.
When to use
- Naming an icon-only control. Set
relationship="label"so the tip is the control's accessible name, not a second description on top of it. - A short supplement to a visible label: a keyboard shortcut, a one-line clarification, the full text of a label that was truncated.
- Hint text a user can miss. A tooltip is hidden until hover or keyboard focus, so everything in it must be optional.
- Dense toolbars. Wrap the group in
TooltipProviderso the second and later tips open instantly once the first has been shown.
When not to use
- For anything a user needs to finish a task. It is absent on touch and easy to miss. Put it in visible text, helper text or a label.
- For interactive content. Links and buttons inside a tip cannot be reached. Use Popover.
- For a rich preview of a person, page or record. Use HoverCard.
- To repeat a visible label. If the control already says it, the tip adds noise.
- On a non-focusable element or a disabled control. Keyboard users never reach it. See Disabled triggers for the workaround.
- For long text. Past a short phrase or sentence, use a Popover or put the text on the page.
Installation
pnpm add @tessinaui/uiUsage
import { Tooltip, TooltipTrigger, TooltipContent } from "@tessinaui/ui";<Tooltip>
<TooltipTrigger render={<Button variant="outline">Save</Button>} />
<TooltipContent>Saves your draft without publishing it</TooltipContent>
</Tooltip>TooltipTrigger renders a <button> by default; render swaps in any element
and keeps the wiring. TooltipContent collapses Base UI's portal, positioner
and popup into one part, and draws the arrow for you unless you pass
arrow={false}.
Rules
These are house rules because the design systems we surveyed agree on them, not because of a local preference.
- Nothing interactive inside. No links, no buttons, nothing focusable — a tooltip closes as the pointer leaves the trigger, so anything in it is a trap. Use Popover for content people need to reach.
- Nothing essential inside. A tooltip is hidden by default, absent on touch, and easy to miss. If a task cannot be completed without the text, it belongs in helper text or a label.
- The trigger must be focusable, so the tip opens for a keyboard user too.
- Keep it short — a phrase or a sentence, sentence case.
- Don't put a tooltip on a disabled control if you can avoid it. See Disabled triggers for the case where you cannot.
Which component?
| You want | Use |
|---|---|
| A short hint on hover/focus | Tooltip |
| Content people tap, click or interact with | Popover |
| A rich preview of an entity on hover | HoverCard |
| A tour step, coach mark or feature callout | CoachMark |
| A value readout on a chart | Chart's own ChartTooltip |
Examples
Default
Sides
side picks the edge; the tip flips automatically when it would collide with
the viewport. (Flipping is switched off in this example so all four stay put.)
Alignment
align slides the tip along the chosen side.
Intents
Five plates — none is the always-dark overlay, and the four semantic values
paint the matching surface. The arrow reads the plate's own published fill, so
it can never drift off the tip.
Delay
delay and closeDelay are separate, and both are inherited by the trigger.
Grouped delays
Wrap a cluster in TooltipProvider: the first tip waits out the delay, then
every neighbour you sweep onto opens instantly for timeout ms. Without it, a
toolbar makes you wait at every single icon.
Keyboard shortcuts
The most common piece of structure inside a real tooltip. Key renders
on-color caps, so they read on the plate with no extra styling.
Naming vs describing an icon button
relationship decides what the tip is to assistive tech, and it is the one
prop worth thinking about on every tooltip you write.
"description"(default) — the tip adds detail to a control that already has a name, viaaria-describedbywhile the tip is open."label"— the tip becomes the trigger's accessible name, for an icon-only button. The text is also rendered into an always-present visually hidden copy, because a name that only exists while the pointer hovers is no name at all; the visible popup is then hidden from AT so nothing is announced twice."none"— no ARIA wiring, for a trigger that is already fully described.
label is for a bare trigger you built yourself. The house
IconButton requires an aria-label or
aria-labelledby in its type — it can never be the unnamed case — so pair a
tooltip on one with description and let it add detail instead.
Disabled triggers
A disabled button fires no pointer events, so a tooltip attached to one never opens. Make the wrapper the trigger instead.
Prefer not to need this. In a survey of real products, nobody explained a disabled control with a hover tip — they used an inline banner or helper text, which a touch user and a screen-reader user can also reach.
On color
The tip is its own on-color plate wherever it opens, so content inside it, like these Key caps, needs no tone prop. On a coloured Surface the trigger inherits on-color as usual.
RTL
dir="rtl" on Tooltip reaches the portalled positioner, so align="start" hugs the trigger's right edge and end its left. side stays physical: left opens on the left in both directions. With dir unset the tip takes the direction around its trigger.
API Reference
Tooltip
The root. Holds the style context and the open state.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Padding and text size |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Corner radius. The arrow's tip radius cascades from it |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Plate colour |
relationship | "description" | "label" | "none" | "description" | How the tip relates to the trigger for AT |
delay | number | 600 | ms before opening on hover |
closeDelay | number | 0 | ms before closing |
open | boolean | — | Controlled open state |
defaultOpen | boolean | — | Uncontrolled initial state |
onOpenChange | (open, event) => void | — | Fires on every open/close |
disabled | boolean | false | The tip never opens. Does not disable the trigger element |
hoverablePopup | boolean | true | Keep the tip open while the pointer is over the tip itself |
trackCursorAxis | "none" | "x" | "y" | "both" | "none" | Follow the cursor instead of anchoring to the trigger's box |
dir | "ltr" | "rtl" | — | Text direction applied to the popup. Unset, it is read around the trigger |
TooltipTrigger
Renders a <button>, or any element via render. Accepts delay and
closeDelay to override the root's, plus every native button prop.
TooltipContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "top" | Which side of the trigger to sit on |
align | "start" | "center" | "end" | "center" | Alignment along that side |
sideOffset | number | 6 | Distance from the trigger, floored to 12 when the arrow is on |
alignOffset | number | 0 | Offset along the side axis |
collisionPadding | number | 8 | Keep this far from the viewport edge |
collisionAvoidance | object | — | Base UI's config. { side: "none" } pins the tip to its declared side |
arrow | boolean | true | Render the pointer |
portal | boolean | true | Render in a portal |
container | HTMLElement | null | document.body | Portal target |
positionerClassName | string | — | Classes for the positioner element |
size, rounded and intent can also be set here to override the root.
TooltipProvider
| Prop | Type | Default | Description |
|---|---|---|---|
delay | number | 600 | Shared open delay for the group |
closeDelay | number | 0 | Shared close delay |
timeout | number | 400 | Window after a tip closes during which the next opens instantly |
TooltipArrow
Rendered for you by TooltipContent. Pass one explicitly to add a className;
yours wins over the automatic one.
It is the house speech-bubble tail — the same contour Popover, HoverCard,
DropdownMenu, CoachMark and NavigationMenu point with, drawn at the smaller of
its two scales because a tip is text rather than a panel. Its apex radius
cascades from rounded, and it paints itself from the plate's own published
fill, so every intent has exactly one colour source. It carries no outline: the
tip has no border for one to continue.
Accessibility
- The popup is
role="tooltip"with an id, and the trigger pointsaria-describedbyat it only while it is open — a permanent reference to an unmounted popup is a dangling IDREF that axe flags. - A caller's own
aria-describedby/aria-labelledbyis kept, and ours appended, never clobbered. - Escape closes the tip.
- The tip opens on keyboard focus, not just hover.
- VoiceOver passed 2026-09-14 (Safari). A description tip reads the name,
then the tip: "Hover me button", "Saves your draft without publishing it".
On an icon button: "Delete Deletes the item for everyone on the team button".
With
relationship="label"the tip replaces the name: "Edit item details button". Chrome was not re-run. NVDA and JAWS are untested — no Windows machine in the loop.