Tooltip
When to use
Section titled “When to use”- The name of an icon-only button
- A keyboard shortcut for a button
- A short hint about a control
Not for anything clickable or long — use a Popover — or essential information that touch users must see.
Choosing between messages and popups:
| Component | Use it for | For example |
|---|---|---|
| Dialog | Something that needs a decision or input before the user carries on; the page is blocked meanwhile | Confirm a delete, edit a record in a form |
| Drawer | A panel that slides in from an edge and blocks the page; the first choice on a phone | Share options, a mobile nav, a filter panel |
| Popover | Extra content or controls next to a button, while the rest of the page stays usable | Filter panel, share options, details card |
| Menu | A list of actions or settings opened from a button | “More actions ⋯”, a View menu |
| Tooltip | A short text label on hover or focus; nothing in it can be clicked | Name of an icon button, a keyboard shortcut |
| HoverCard | A preview of where a link goes, on hover or focus; nothing essential in it | A profile behind an @mention, a repository’s summary |
| Toast | A brief confirmation after an action, which goes away by itself | “Saved”, “Archived — Undo” |
| Alert | A message that stays in the page until the situation changes | “Your trial ends in 3 days”, a form error summary |
Tooltip shows a short label next to an element while it is hovered or has keyboard focus — a
button’s shortcut, or the name of an icon. It is anchored like Popover,
by CSS, and describes the element to screen readers whether it is showing or not.
import { Button, Tooltip } from '@arun-dev/ui';
const TOOLS = [ { label: 'Bold', hint: 'Bold (⌘B)', glyph: 'B', style: { fontWeight: 700 } }, { label: 'Italic', hint: 'Italic (⌘I)', glyph: 'I', style: { fontStyle: 'italic' } }, { label: 'Underline', hint: 'Underline (⌘U)', glyph: 'U', style: { textDecoration: 'underline' }, },];
export default function TooltipToolbar() { return ( <div role="toolbar" aria-label="Formatting" style={{ display: 'flex', gap: 'var(--space-3xs)' }} > {TOOLS.map(({ label, hint, glyph, style }) => ( <Tooltip.Root key={label}> {/* The button's name comes from aria-label; the tooltip adds the shortcut. */} <Tooltip.Trigger render={<Button aria-label={label} />}> <span aria-hidden style={style}> {glyph} </span> </Tooltip.Trigger> <Tooltip.Popup>{hint}</Tooltip.Popup> </Tooltip.Root> ))} </div> );}Anatomy
Section titled “Anatomy”import { Tooltip } from '@arun-dev/ui';
<Tooltip.Root> <Tooltip.Trigger /> <Tooltip.Popup /></Tooltip.Root>;| Part | Renders | Does |
|---|---|---|
Root | nothing | holds open and the hover timers |
Trigger | <button> | the element described; carries aria-describedby and the anchor |
Popup | <div> | the label: popover="manual", role="tooltip", placed by side/align |
The Trigger is usually an element that already exists — render it through:
<Tooltip.Trigger render={<Button aria-label="Bold" />}>. It works on a
<Popover.Trigger> too, and the tooltip steps aside when it is pressed.
When it shows
Section titled “When it shows”| Input | Opens | Closes |
|---|---|---|
| mouse or pen | after resting for delay (600ms) | closeDelay (100ms) after leaving |
| keyboard focus | at once | on blur |
| touch | never — there is no hover to end it | |
| a press on it | at once | |
| Esc | at once — and only the tooltip |
Moving straight from one tooltip to the next skips the delay, so reading along a toolbar doesn’t mean waiting on each. The pointer can move onto the tooltip without it closing, so its text can be read and selected (WCAG 1.4.13).
Esc closes the tooltip without closing a Dialog or Popover underneath it.
What goes in it
Section titled “What goes in it”Text only. A tooltip describes the element and is never focused, so a link or button inside it could not be reached by keyboard. For anything interactive, use a Popover.
Don't put the name here
Give the Trigger its own accessible name — visible text, or aria-label on an icon button. The
tooltip is its description: read after the name, and not at all by some screen readers in some
modes. A button named only by its tooltip may be announced as just “button”.
Nothing essential should live only in a tooltip either: touch users never see one.
Events
Section titled “Events”Where to attach them. Each part renders a real element and passes your props on to it, so any
event that element fires — not only those listed here — is a prop on that part. Tooltip.Root
renders nothing, so it has only onOpenChange.
| Part | Renders | Its events, for example |
|---|---|---|
Tooltip.Popup | <div popover> | onBeforeToggle, onToggle, onTransitionEnd |
Tooltip.Trigger | <button> | onClick, onFocus, onPointerEnter |
onOpenChange on the Root reports every open and close, after any delay has run.
Tooltip.Popup also takes the popover events, onBeforeToggle and onToggle, which fire on each
open and close with event.newState set to 'open' or 'closed'. Stopping an open in
onBeforeToggle keeps the tooltip hidden, and onOpenChange(false) reports it.
Placement
Section titled “Placement”side picks the side of the trigger, and align where along it: flush with its start or
end edge, or centred. With no room on the chosen side, it flips to the opposite one.
<Tooltip.Popup side="top" align="center">Saves the draft</Tooltip.Popup>Tooltip.Root
Section titled “Tooltip.Root”| Prop | Type | Default | Description |
|---|---|---|---|
delay | number | 600 | Milliseconds a pointer rests on the trigger before it opens. Keyboard focus opens it at once, and moving straight from another tooltip skips the delay. |
closeDelay | number | 100 | Milliseconds between the pointer leaving and the tooltip closing — time to move onto the tooltip, which stays open while hovered. |
open | boolean | Controlled state. Provide onOpenChange alongside it. Never pass undefined — the mode is fixed at mount. | |
defaultOpen | boolean | false | Initial state when uncontrolled. Read once, at mount. |
onOpenChange | (open: boolean) => void | Called on every open and close, after any delay has run. | |
children | ReactNode | The other parts. Root renders no element of its own. |
Tooltip.Popup
Section titled “Tooltip.Popup”| Prop | Type | Default | Description |
|---|---|---|---|
side | 'top' | 'bottom' | 'left' | 'right' | 'top' | Which side of the Trigger to show on. Flips to the opposite side when there is no room. |
align | 'start' | 'center' | 'end' | 'center' | Flush with the Trigger's start or end edge, or centred on it. |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
Tooltip.Trigger takes the props of the <button> it renders, plus render and ref. An
aria-describedby of yours is kept, with the tooltip’s id added to it.
- Placement works as for Popover:
sidedefaults totop, it flips when there’s no room, and an older browser without anchor positioning shows it centred rather than beside the element. - A disabled
<button>receives no pointer or focus events, so its tooltip can’t show. Explain why an action is unavailable in text near it instead. - Restyle every tooltip with the
--tooltip-*tokens, e.g.--tooltip-max-width.