Tooltips need text
- Rule:
axe/aria-tooltip-name - Checks: ARIA tooltip nodes must have an accessible name
- WCAG: 4.1.2
- Fix class:
content: A fix needs authored text.
Who it affects
Section titled “Who it affects”- People who use a screen reader or a braille display, most of them blind.
How to fix it
Section titled “How to fix it”The tooltip stays on the page, empty, until it opens
Section titled “The tooltip stays on the page, empty, until it opens”Hide the tooltip with the hidden attribute or display: none while it is closed, and fill it with text before you show it. A tooltip hidden only with opacity or moved off screen is still in the accessibility tree. Link it from its trigger with aria-describedby.
<button type="button" aria-describedby="copy-tip">Copy link</button><div role="tooltip" id="copy-tip" hidden>Copies the page address to your clipboard</div>The tooltip holds only an icon or image
Section titled “The tooltip holds only an icon or image”Put the words in the tooltip as text, or give an image inside it an alt that says them.
Fixes that fail
Section titled “Fixes that fail”Adding aria-label to an empty tooltip
Section titled “Adding aria-label to an empty tooltip”axe accepts it, but sighted users still see an empty box when the tooltip opens. Put the text in the tooltip itself, where everyone gets it.
In React
Section titled “In React”The part of the fix that differs in React, in its syntax. The rest of the guidance holds.
A tooltip component keeps an empty role="tooltip" element mounted and fills it only when it opens
Section titled “A tooltip component keeps an empty role="tooltip" element mounted and fills it only when it opens”Render the tooltip’s text from the start and toggle hidden={!open}, so the element is hidden while closed and the trigger’s aria-describedby, with an id from useId, always finds it. Keep it open while the trigger has focus or the pointer is over the trigger or the tooltip, and close it on Escape.
import { useEffect, useId, useState } from 'react';
function CopyLinkButton() { const tipId = useId(); const [focused, setFocused] = useState(false); const [hovered, setHovered] = useState(false); const [dismissed, setDismissed] = useState(false); const open = (focused || hovered) && !dismissed;
useEffect(() => { if (!open) return; const onKeyDown = (event) => event.key === 'Escape' && setDismissed(true); document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); }, [open]);
return ( <span onMouseEnter={() => { setHovered(true); setDismissed(false); }} onMouseLeave={() => setHovered(false)} > <button type="button" aria-describedby={tipId} onFocus={() => { setFocused(true); setDismissed(false); }} onBlur={() => setFocused(false)} > Copy link </button> <span role="tooltip" id={tipId} hidden={!open}> Copies the page address to your clipboard </span> </span> );}Fixes that fail in React
Section titled “Fixes that fail in React”Mounting the tooltip only while it is open, with the trigger always pointing at it
Section titled “Mounting the tooltip only while it is open, with the trigger always pointing at it”While the tooltip is closed, aria-describedby names an element that is not in the page, and when it opens on focus, screen readers may already have announced the trigger without it.
More on the rule
Section titled “More on the rule”axe-core’s documentation of the rule
CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.