// pattern
Accessible Tooltip Pattern
A tooltip that displays supplementary text on hover and focus using role="tooltip" and aria-describedby. Includes a toggletip variant for dynamic content with aria-live. Dismissable with Escape per WCAG 1.4.13.
// 01 · live demo
Live Demo
Hover or focus each trigger to see the tooltip. Press Escape to dismiss.
ARIA roles tell assistive technology what an element does. Use them only when no native HTML element provides the same semantics.
Inaccessible version (title attribute):
This div uses only the title attribute. It cannot be focused with a keyboard, the tooltip cannot be dismissed, it cannot be styled, and screen readers may ignore it entirely.
// 02 · the code
The Code
<!-- Tooltip on an icon button -->
<div class="tooltip-wrap">
<button type="button"
aria-label="Settings"
aria-describedby="tip-settings">
<!-- icon SVG -->
</button>
<span role="tooltip" id="tip-settings">
Settings
</span>
</div>
<!-- Tooltip on an abbreviation -->
<div class="tooltip-wrap">
<span tabindex="0"
aria-describedby="tip-wcag">
WCAG
</span>
<span role="tooltip" id="tip-wcag">
Web Content Accessibility Guidelines
</span>
</div>
<!-- Toggletip: button + live region -->
<div class="toggletip-wrap">
<button type="button"
aria-label="More info about ARIA roles">
?
</button>
<div role="status" hidden>
<p>ARIA roles tell assistive technology
what an element does.</p>
</div>
</div>
.tooltip-wrap {
position: relative;
display: inline-block;
}
/* Tooltip bubble */
.tooltip-wrap [role="tooltip"] {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.5rem;
background: #1f2937;
color: #fff;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
border-radius: 0.25rem;
pointer-events: none;
opacity: 0;
transition: opacity 150ms ease;
z-index: 10;
}
/* Arrow pointing down */
.tooltip-wrap [role="tooltip"]::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #1f2937;
}
/* Show on hover AND focus-within (SC 1.4.13) */
.tooltip-wrap:hover [role="tooltip"],
.tooltip-wrap:focus-within [role="tooltip"] {
opacity: 1;
}
/* Hidden state set by Escape key (JS) */
.tooltip-wrap [role="tooltip"].tooltip-hidden {
opacity: 0 !important;
}
/* SC 2.4.13: visible focus indicator */
.tooltip-wrap button:focus-visible,
.tooltip-wrap [tabindex="0"]:focus-visible {
outline: 2px solid #5b2a86;
outline-offset: 2px;
border-radius: 0.25rem;
}
/* SC 2.5.8: minimum target size */
.tooltip-wrap button {
min-width: 2.75rem;
min-height: 2.75rem;
}
/* Toggletip bubble */
.toggletip-wrap [role="status"] {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 0.75rem;
background: #fff;
border: 2px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
width: 240px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
z-index: 10;
}
.toggletip-wrap [role="status"][hidden] {
display: none;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.tooltip-wrap [role="tooltip"] {
transition: none;
}
}
// Escape key dismisses all visible tooltips
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('[role="tooltip"]')
.forEach(tip => tip.classList.add('tooltip-hidden'));
// Also close any open toggletips
document.querySelectorAll(
'.toggletip-wrap [role="status"]'
).forEach(bubble => {
bubble.hidden = true;
});
}
});
// Remove the hidden class when trigger is
// hovered or focused again
document.querySelectorAll('.tooltip-wrap')
.forEach(wrap => {
const tip = wrap.querySelector('[role="tooltip"]');
function show() {
tip.classList.remove('tooltip-hidden');
}
wrap.addEventListener('mouseenter', show);
wrap.addEventListener('focusin', show);
});
// Toggletip: click to show/hide
document.querySelectorAll('.toggletip-wrap')
.forEach(wrap => {
const btn = wrap.querySelector('button');
const bubble = wrap.querySelector('[role="status"]');
btn.addEventListener('click', () => {
bubble.hidden = !bubble.hidden;
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (!wrap.contains(e.target)) {
bubble.hidden = true;
}
});
});
role="status" on the toggletip?
The role="status" implicitly sets aria-live="polite", so screen readers announce the content when it appears without interrupting the current speech. This is better than role="tooltip" for the toggletip because the content is triggered by an explicit user action (click), not by hover/focus.
// 03 · why these decisions
Why These Decisions
Why role="tooltip"
The role="tooltip" tells assistive technology that the popup element contains supplementary description text for its trigger. Without this role, a screen reader sees a generic <span> and has no way to know it functions as a tooltip. The role also establishes the expected interaction pattern: the content is informational, not interactive, and is tied to the trigger element.
Why aria-describedby instead of aria-labelledby
A tooltip supplements the trigger's existing accessible name — it does not replace it. aria-describedby adds extra description that screen readers announce after the name and role. If you used aria-labelledby instead, the tooltip text would override the trigger's accessible name. For example, a "Settings" button with an aria-labelledby tooltip would lose its "Settings" label entirely.
Why show on both hover and focus
WCAG SC 1.4.13 (Content on Hover or Focus) explicitly requires that content triggered by pointer hover must also be triggerable by keyboard focus. Showing the tooltip only on hover locks out keyboard users entirely. Using CSS :hover and :focus-within together ensures both input methods work without any JavaScript for the basic show/hide behavior.
Why Escape to dismiss
WCAG SC 1.4.13 has three requirements for hover/focus content: it must be dismissable, hoverable, and persistent. The Escape key satisfies the dismissable requirement — the user can hide the tooltip without moving the pointer or focus away from the trigger. This is critical when a tooltip obscures other content the user needs to read.
Why not the title attribute
The title attribute is the native HTML tooltip, but it has serious accessibility problems. It cannot be triggered by keyboard focus in most browsers. It cannot be dismissed once shown. It cannot be styled, so it often has poor contrast or gets truncated for long text. Many screen readers ignore it entirely when an accessible name is already present. The WAI explicitly advises against relying on title for important information.
Why the toggletip is a separate pattern
Standard tooltips are for short, supplementary text — they appear on hover/focus and use aria-describedby. Toggletips are for longer descriptions or content that requires deliberate action to read. They use a button trigger with click activation and an aria-live region to announce the content. Mixing these patterns (e.g., putting interactive content in a tooltip) creates confusion for screen reader users who expect tooltips to be passive text.
// 04 · keyboard interaction
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Moves focus to the tooltip trigger, showing the tooltip |
| Escape | Dismisses the tooltip without moving focus away from the trigger |
| Tab (away) | Moves focus to the next focusable element, hiding the tooltip |
| Enter / Space | For toggletip: activates the toggle button, showing or hiding the popup content |
// 05 · wcag 2.2 success criteria
WCAG 2.2 Success Criteria
This pattern satisfies the following WCAG 2.2 success criteria:
- 1.4.13 Content on Hover or Focus Level AA — Tooltip appears on hover and focus, remains visible while the pointer is over the tooltip, and can be dismissed with Escape without moving focus.
- 2.1.1 Keyboard Level A — Tooltip is accessible via keyboard focus on the trigger. Toggletip activates with Enter or Space.
-
1.3.1 Info and Relationships
Level A
—
aria-describedbyprogrammatically links the tooltip text to its trigger element, so the relationship is exposed to assistive technology. - 2.4.13 Focus Appearance Level AAA — Trigger elements have a visible 2px solid focus indicator with sufficient contrast ratio.
-
2.5.8 Target Size (Minimum)
Level AA
— Button triggers meet the 24x24px minimum target size via
min-widthandmin-heightof2.75rem. -
4.1.2 Name, Role, Value
Level A
—
role="tooltip"identifies the popup's purpose. The trigger's accessible name and description are programmatically determinable. - 2.4.11 Focus Not Obscured (Minimum) Level AA — Tooltip is positioned above the trigger so it does not cover other focused elements or the trigger itself.
// 06 · screen reader behavior
Screen Reader Behavior
When focusing the icon button (tooltip)
- NVDA: "Settings, button" — announces the accessible name and role, then after a brief pause — "Settings" — reads the
aria-describedbycontent as description - JAWS: "Settings, button, Settings" — announces the button label, then the tooltip description
- VoiceOver: "Settings, button" — announces the label and role, then "Settings" as the description
When focusing the abbreviation (tooltip)
- NVDA: "WCAG" — announces the text content, then "Web Content Accessibility Guidelines" — reads the description from
aria-describedby - JAWS: "WCAG, Web Content Accessibility Guidelines" — announces the text and its description together
- VoiceOver: "WCAG" — announces the text, then "Web Content Accessibility Guidelines" as description
When activating the toggletip
- NVDA: Announces the live region content politely after the button activation — "ARIA roles tell assistive technology what an element does..."
- JAWS: Announces the status content when it appears — reads the full text of the live region
- VoiceOver: "More info about ARIA roles, button" on focus, then announces the live region content when the popup becomes visible
Description timing
Screen readers typically announce aria-describedby content after the element's name and role, often with a brief pause. Some screen readers (like NVDA) allow users to configure whether descriptions are read automatically or only on demand with a keyboard shortcut. This is why aria-describedby is preferred for supplementary information — it doesn't interfere with the primary label announcement.
// 07 · common mistakes
Common Mistakes
title attribute
The title attribute tooltip is unreliable across browsers and assistive technology. It cannot be triggered by keyboard focus, cannot be dismissed, cannot be styled, is often truncated for longer text, and many screen readers skip it when other accessible name sources exist. Use role="tooltip" with aria-describedby instead.
:focus-within on the tooltip wrapper or :focus on the trigger to ensure keyboard parity.
aria-labelledby instead of aria-describedby
aria-labelledby overrides the element's accessible name entirely. If a button has aria-label="Settings" and you also add aria-labelledby pointing to a tooltip, the tooltip text replaces "Settings" as the name. Use aria-describedby to add supplementary information without changing the name.
// 08 · native html vs. aria
Native HTML vs. ARIA
HTML provides the title attribute as a native tooltip, but it falls short of accessibility requirements. This table compares the native approach with the ARIA tooltip pattern.
| Feature | Native HTML (title) |
ARIA (role="tooltip") |
|---|---|---|
| Tooltip element | title attribute (browser-rendered popup) |
<span role="tooltip"> with aria-describedby |
| Keyboard trigger | None — title does not show on focus in most browsers |
CSS :focus-within shows tooltip when trigger receives focus |
| Dismiss mechanism | None — cannot be dismissed without moving the pointer | Escape key via JavaScript hides the tooltip |
| Styling | None — browser default, cannot be customized | Full CSS control over colors, positioning, animation, and arrow |
| Hover persistence | Browser-controlled — often disappears after a timeout | CSS :hover keeps tooltip visible while pointer is over trigger |
| Screen reader support | Inconsistent — often ignored when aria-label is present |
aria-describedby reliably announces tooltip content |
| Long text handling | Often truncated by the browser | Full text always shown, line wrapping controlled by CSS |
| Mobile support | No hover on touch — title is typically invisible |
Can add touch handlers or use toggletip variant for mobile |
title attribute fails nearly every WCAG requirement for tooltip content. Use role="tooltip" with aria-describedby for accessible tooltips, and a toggletip with role="status" for longer or dynamic content. The native title can be added as a progressive enhancement but should never be the only tooltip mechanism.