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
mouseenterandfocus; closes onmouseleaveandblur. Keyboard users get the same information mouse users do. - Semantic role. The popup uses
role="tooltip"and the trigger getsaria-describedbypointing at it while open. - Single-tab-stop trigger. The default trigger is
tabIndex=0on aspanwhen no interactive child is provided — but you should normally pass a real<button>as the child instead. - Describes, doesn't label.
aria-describedbymeans 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.
TooltipContenthas its ownonMouseEnter/onMouseLeavehandlers that pause and re-arm a 100 ms close timer, so the smallmb-2gap 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
Escapecloses 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 — respectprefers-reduced-motionif 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.