Skip to content

Tooltip

  • 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:

ComponentUse it forFor example
DialogSomething that needs a decision or input before the user carries on; the page is blocked meanwhileConfirm a delete, edit a record in a form
DrawerA panel that slides in from an edge and blocks the page; the first choice on a phoneShare options, a mobile nav, a filter panel
PopoverExtra content or controls next to a button, while the rest of the page stays usableFilter panel, share options, details card
MenuA list of actions or settings opened from a button“More actions ⋯”, a View menu
TooltipA short text label on hover or focus; nothing in it can be clickedName of an icon button, a keyboard shortcut
HoverCardA preview of where a link goes, on hover or focus; nothing essential in itA profile behind an @mention, a repository’s summary
ToastA brief confirmation after an action, which goes away by itself“Saved”, “Archived — Undo”
AlertA 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>
);
}
import { Tooltip } from '@arun-dev/ui';
<Tooltip.Root>
<Tooltip.Trigger />
<Tooltip.Popup />
</Tooltip.Root>;
PartRendersDoes
Rootnothingholds 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.

InputOpensCloses
mouse or penafter resting for delay (600ms)closeDelay (100ms) after leaving
keyboard focusat onceon blur
touchnever — there is no hover to end it
a press on itat once
Escat 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.

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.

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.

PartRendersIts 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.

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 props
PropTypeDefaultDescription
delaynumber600Milliseconds a pointer rests on the trigger before it opens. Keyboard focus opens it at once, and moving straight from another tooltip skips the delay.
closeDelaynumber100Milliseconds between the pointer leaving and the tooltip closing — time to move onto the tooltip, which stays open while hovered.
openbooleanControlled state. Provide onOpenChange alongside it. Never pass undefined — the mode is fixed at mount.
defaultOpenbooleanfalseInitial state when uncontrolled. Read once, at mount.
onOpenChange(open: boolean) => voidCalled on every open and close, after any delay has run.
childrenReactNodeThe other parts. Root renders no element of its own.
Tooltip.Popup props
PropTypeDefaultDescription
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.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything 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: side defaults to top, 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.