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

AttributeWhen to use
role="tooltip"On the tooltip container
aria-describedbyOn the trigger, points to the tooltip β€” for supplementary text
aria-labelledbyOn the trigger, points to the tooltip β€” when the tooltip IS the label
aria-expandedOn the trigger for toggletips only
aria-controlsOn 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 title attribute 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> with tabindex="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.

- Vinay Ranjan