Accessible tooltips and popovers
Tooltips provide supplementary information when users hover or focus on an element. When built incorrectly, this information becomes invisible to keyboard and screen reader users.
The difference between tooltips and toggletips
Tooltip β Appears on hover/focus, provides supplementary text, is not interactive :
<button aria-describedby="save-tip">
πΎ
</button>
<div role="tooltip" id="save-tip">Save document</div>Toggletip β Appears on click, can contain interactive content (links, buttons) :
<button aria-expanded="false" aria-controls="info-panel">
βΉοΈ More info
</button>
<div id="info-panel" role="region" hidden>
<p>This feature requires a <a href="/upgrade">premium plan</a>.</p>
</div>Building an accessible tooltip
HTML structure
<span class="tooltip-trigger" tabindex="0" aria-describedby="tip-1">
WCAG
</span>
<span role="tooltip" id="tip-1" class="tooltip">
Web Content Accessibility Guidelines
</span>CSS
.tooltip-trigger {
position: relative;
text-decoration: underline dotted;
cursor: help;
}
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 0.75rem;
background: #1a202c;
color: #fff;
border-radius: 0.375rem;
font-size: 0.85rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
.tooltip-trigger:hover .tooltip,
.tooltip-trigger:focus .tooltip {
opacity: 1;
}Keyboard support
The tooltip must appear on focus, not just hover :
.tooltip-trigger:focus-visible .tooltip {
opacity: 1;
}The Escape key should dismiss the tooltip without moving focus :
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.tooltip').forEach(tip => {
tip.style.opacity = '0';
});
}
});The Popover API
The Popover API provides built-in accessibility for interactive popovers :
<button popovertarget="my-popover">Help</button>
<div id="my-popover" popover>
<h3>Keyboard shortcuts</h3>
<ul>
<li><kbd>Ctrl+S</kbd> β Save</li>
<li><kbd>Ctrl+Z</kbd> β Undo</li>
</ul>
</div>The Popover API automatically :
- Closes on Escape
- Closes when clicking outside
- Manages the top layer (no z-index issues)
- Works with screen readers
ARIA attributes for tooltips
| Attribute | When to use |
|---|---|
role="tooltip" | On the tooltip container |
aria-describedby | On the trigger, points to the tooltip β for supplementary text |
aria-labelledby | On the trigger, points to the tooltip β when the tooltip IS the label |
aria-expanded | On the trigger for toggletips only |
aria-controls | On the trigger, points to the toggletip content |
Timing and persistence
Tooltips must remain visible long enough to be read :
- Show after a short delay (300β500ms) to prevent flickering
- Keep visible while the element has hover or focus
- Donβt hide on a timer β let the user dismiss naturally
.tooltip-trigger:hover .tooltip {
opacity: 1;
transition-delay: 0.3s;
}
.tooltip-trigger:not(:hover) .tooltip {
transition-delay: 0s;
}Common mistakes
- Hover-only tooltips β Must also appear on keyboard focus.
- Interactive content in tooltips β Tooltips should contain only text. Use a toggletip or popover for links and buttons.
- Custom title attributes β The
titleattribute has poor screen reader support and cannot be styled. Avoid it. - Tooltips on disabled elements β Disabled buttons canβt receive focus. Wrap in a
<span>withtabindex="0"instead. - Tooltips that obscure content β Position tooltips so they donβt cover adjacent elements or the trigger itself.
Resources
If you found this helpful, share it with someone who's building for the web.