Skip to content

Tooltip

A small popup that surfaces supplementary information for an icon, control, or truncated label. Built on the WAI-ARIA tooltip pattern with hover and focus triggers.

Tooltips clarify icons, abbreviated labels, and truncated text. The component shows a popup on hover and keyboard focus, wires aria-describedby from the trigger to the popup, and renders the popup with role="tooltip".

When (and When Not) to Use

Use a tooltip for

  • Supplementary hints. "Format as bold (Ctrl+B)" on a toolbar button — useful, but the icon is still recognisable without it.
  • Disambiguating truncated text. A long file name shortened to "report-2026‑Q1…" can show the full string on hover.
  • Static, plain-text content. Anything more complex belongs in a popover or dialog.

Don't use a tooltip for

  • Labelling an icon-only button. The button still needs a real accessible name (aria-label). The tooltip is supplementary, not a replacement — touch users and many AT users may never see it.
  • Critical information. Form errors, disabled-reason explanations, or anything required to complete the task — these must be visible without hover.
  • Interactive content. Links, buttons, or form fields inside a tooltip are unreachable by keyboard and dismissed on hover-out. Use a popover or dropdown instead.
  • Long content. Anything beyond a short sentence belongs in a different pattern — tooltips overflow viewports and disappear before users finish reading.

Accessibility Features

  • Hover and focus triggers. Opens on mouseenter and focus; closes on mouseleave and blur. Keyboard users get the same information mouse users do.
  • Semantic role. The popup uses role="tooltip" and the trigger gets aria-describedby pointing at it while open.
  • Single-tab-stop trigger. The default trigger is tabIndex=0 on a span when no interactive child is provided — but you should normally pass a real <button> as the child instead.
  • Describes, doesn't label. aria-describedby means the tooltip adds to the trigger's accessible name — it does not replace it. The trigger still needs its own label.

WCAG 1.4.13: Content on Hover or Focus

Success Criterion 1.4.13 (Level AA) requires three things from any content that appears on hover or focus:

  • Hoverable. The user can move the pointer from the trigger onto the tooltip without it disappearing. TooltipContent has its own onMouseEnter/onMouseLeave handlers that pause and re-arm a 100 ms close timer, so the small mb-2 gap between trigger and content is bridged automatically. The tooltip stays visible as long as the pointer is over either the trigger or the content. ✅
  • Persistent. The tooltip remains visible until the trigger loses hover/focus or the user dismisses it — it does not auto-close on a timer. ✅
  • Dismissible. Pressing Escape closes the tooltip without moving pointer or focus. The component also tracks the dismissed state so the tooltip does not immediately re-open while the trigger is still hovered or focused — the user must move the pointer away (or blur the trigger) before it will appear again.

Usage Examples

Icon-only button (with real aria-label)

import { Tooltip, TooltipTrigger, TooltipContent } from '@holmdigital/components'; <Tooltip> <TooltipTrigger> <button aria-label="Delete item" onClick={handleDelete}> <TrashIcon aria-hidden="true" /> </button> </TooltipTrigger> <TooltipContent>Delete this item permanently</TooltipContent> </Tooltip>

The button has its own aria-label="Delete item" — the tooltip just describes what the action does. Screen readers announce both: name first, description second.

Truncated text

<Tooltip> <TooltipTrigger> <span className="truncate max-w-[160px] inline-block align-bottom"> report-2026-Q1-accessibility-audit-final.pdf </span> </TooltipTrigger> <TooltipContent>report-2026-Q1-accessibility-audit-final.pdf</TooltipContent> </Tooltip>

Keyboard shortcut hint

<Tooltip> <TooltipTrigger> <button onClick={save}>Save</button> </TooltipTrigger> <TooltipContent>Save (Ctrl + S)</TooltipContent> </Tooltip>

Touch & Mobile

Tooltips do not have a reliable touch trigger. There is no "hover" on a phone, and many touch devices fire focus only on certain element types — meaning the tooltip may simply never appear. Anything you put in a tooltip must be redundant with information that is accessible without it.

Props

The component is composed of three pieces:

// Root — wraps trigger and content, manages open state. <Tooltip>{children}</Tooltip> // Trigger — opens on hover/focus. Pass an interactive child // (button/link). If you pass plain text, the component renders // a focusable span with tabIndex=0. <TooltipTrigger asChild?: boolean>{children}</TooltipTrigger> // Content — the popup. Rendered with role="tooltip" and connected // to the trigger via aria-describedby. Hidden when closed. <TooltipContent className?: string>{children}</TooltipContent> // Optional Provider — currently a passthrough; reserved for // future delay/grouping behaviour. <TooltipProvider>{children}</TooltipProvider>

Best Practices

  • Always pass an interactive trigger. A real <button> or <a> with its own accessible name — never rely on the tooltip text alone.
  • Keep content short. One line, plain text. If you need formatting, headings, or links, use a popover.
  • Don't repeat the visible label. If a button already says "Save", a tooltip that says "Save" is noise. Add the shortcut or behaviour ("Save (Ctrl + S)") instead.
  • Test with the keyboard alone. Tab to the trigger — does the tooltip appear? Press Escape — does it close? Both must work.
  • Mind contrast and motion. The default styling pairs white text on slate-900 (≥ 15:1 contrast) and animates with a brief fade — respect prefers-reduced-motion if you customise the animation.

Common Mistakes

Using a tooltip as the only label for an icon-only button. Touch users and many AT users will never see it. Always set aria-label on the button.
Putting interactive content inside a tooltip. Links and buttons inside a tooltip are unreachable — the tooltip closes the moment the user moves the pointer or focus toward them.
Hiding critical info in a tooltip. Disabled-button reasons, error messages, and required formatting belong in always-visible help text, not a hover popup.
Wrapping a non-focusable element without a fallback. A tooltip on a plain<span> with no tabIndex won't open for keyboard users — pass an interactive child or accept the default focusable wrapper.