When to use
A tooltip is supplementary information for a control. The user should be able to use the control without reading the tooltip — if they can’t, the tooltip’s content belongs in the visible label.
Use a tooltip when:
- A button’s label is necessarily an icon, and you want sighted hover users to see the full description (the screen reader gets it via
aria-labelor text content). - You want to provide the longer “why” for a control whose visible label is intentionally short.
Don’t use a tooltip when:
- The information is essential to the operation — put it in the visible label or as a hint paragraph.
- The information is long, formatted, or has links — use a popover or
<dialog>instead. - The control is not focusable — tooltips on plain text are unreachable for keyboard users.
The pattern
The minimum viable accessible tooltip is three things wired together:
- A focusable host element (a
<button>, an input, a link). - A tooltip element with
role="tooltip"and anid. - The host points at it via
aria-describedby="<tooltip-id>".
<button type="button" aria-describedby="help-tip">
?
</button>
<span id="help-tip" role="tooltip">
We use this for the "what is this" hover.
</span> aria-describedby makes the screen reader announce the tooltip text after the button’s name when focus arrives. CSS handles visibility on hover and focus:
.tooltip {
opacity: 0;
visibility: hidden;
transition: opacity 100ms;
}
button:hover .tooltip,
button:focus-visible .tooltip,
.tooltip-host:hover .tooltip,
.tooltip-host:focus-within .tooltip {
opacity: 1;
visibility: visible;
} Note :focus-within on the wrapping host — it lets the tooltip stay visible while focus is anywhere inside (useful when the host is a small wrapper around an input).
Three rules from WCAG 1.4.13
- Hoverable — pointing at the tooltip itself must not dismiss it. (Otherwise users with low-precision pointers can’t read it.) Use a wrapping host so leaving the trigger doesn’t immediately hide the tooltip if the cursor moves toward the tip.
- Dismissable — Escape must close the tooltip without moving focus. Listen on the host:
button.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
button.blur();
button.focus(); // keep focus, but lose hover
}
}); - Persistent — the tooltip stays until the user dismisses it, moves focus, or moves the pointer. Don’t auto-dismiss on a timeout.
When the icon is the label
For an icon-only button, the visible icon is decorative and the accessible name comes from aria-label. Tooltip then describes more, not the same thing:
<button type="button" aria-label="Bookmark" aria-describedby="bk-tip">
★
</button>
<span id="bk-tip" role="tooltip">Save for later</span> Screen reader announces: “Bookmark, button. Save for later.” The label names what the button does (“bookmark”); the tooltip describes the consequence (“save for later”).
Anti-patterns
title attribute as a tooltip
<button type="button" title="Save for later">★</button> Three problems:
- Not visible on focus — keyboard users only see it if they ALSO hover, which they probably can’t.
- Not dismissable — no way to close it without moving the pointer.
- Inconsistent SR support — some screen readers announce
title, some don’t, some announce it only after the accessible name. Don’t rely on it for anything important.
The title attribute is fine as a fallback for mouse users when no real tooltip is implemented. It’s not a tooltip.
CSS-only tooltips on non-focusable elements
<span class="tooltip-host">
Hover me
<span class="tooltip">A definition</span>
</span> If .tooltip-host isn’t focusable, keyboard users can’t reveal the tooltip. Either make the element focusable (and meaningful — usually a <button type="button"> or <a>), or put the information in the visible content.
Tooltip with a “click to close” inside
The moment the tooltip contains a link or a button, it’s not a tooltip — it’s a popover. Tooltips have pointer-events: none and can’t receive interaction. For “click to learn more” or “close button” you want a <dialog> or a custom popover with focus management. Mixing the two breaks the tooltip role contract.
Common variations
- Form-field hint — not a tooltip. Visible all the time, linked via
aria-describedbyto a visible paragraph. Use the form field pattern instead. - Truncated text reveal — when text is cut off with ellipsis, a tooltip showing the full string is OK if the truncated version is itself meaningful. If the truncation hides essential content, fix the layout instead.
- Disabled buttons — disabled buttons aren’t focusable, so a tooltip on them is unreachable. Either use
aria-disabled="true"(still focusable) so the tooltip explains why, or move the explanation elsewhere.
Checklist
- Host element is focusable (
<button>,<a>, input) - Tooltip has
role="tooltip"and anid - Host has
aria-describedby="<tooltip-id>" - Tooltip appears on both hover and focus, not one or the other
- Tooltip is hoverable (cursor can enter it without dismissing)
- Escape dismisses it without moving focus
- Tooltip persists until the user dismisses it (no auto-hide timer)
- Tooltip content is supplementary — never the only source of essential info
- No interactive content inside (no links, no buttons — that’s a popover)