tooltips — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited tooltips (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Canonical source:examples/TOOLTIP_ACCESSIBILITY_BEST_PRACTICES.mdinmgifford/ACCESSIBILITY.mdThis skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules when implementing or reviewing tooltip components. Only load this skill if the project contains tooltips.
A tooltip is a short, non-interactive description that appears automatically when its trigger receives keyboard focus or pointer hover. An accessible tooltip must be available to keyboard, pointer, touch, magnification, and assistive technology users without hiding essential information or blocking the interface.
Tooltips are often the wrong solution. Persistent visible text, a clear control label, a disclosure, or a dialog is usually easier to discover and use. This guide covers authored tooltips; the HTML title attribute produces a browser-controlled tooltip with different behavior and limitations (see below).
Use the project's skills/bug-reporting/SKILL.md severity/priority framework to grade findings. The severity scale below is a skill-local shorthand, not a universal standard.
| Level | Meaning |
|---|---|
| Critical | Tooltip contains essential information with no other access route |
| Serious | Tooltip unreachable by keyboard or AT; interactive content trapped in a tooltip |
| Moderate | Tooltip accessible but WCAG 1.4.13 requirements not fully met |
| Minor | Best-practice gap; marginal impact |
Calling every small popup a tooltip leads to incorrect semantics and keyboard behavior. Classify the interaction before choosing ARIA:
| Need | Use |
|---|---|
| A control needs a clearer label | Improve its visible label |
| Short supplementary text should appear automatically on focus or hover | Tooltip |
| Instructions or requirements are needed to complete a task | Persistent visible help text |
| A user explicitly asks to show or hide short help | Button-controlled disclosure |
| The popup contains links, buttons, or other controls | Non-modal dialog or another appropriate popup pattern |
| A decision is required before work can continue | Modal dialog or alert dialog |
| An action completed or a process changed state | Status message, not a tooltip |
| Text is truncated only because of layout | Allow wrapping, expansion, or another way to read the full text |
An authored tooltip: describes an element, appears automatically after focus or hover, contains only non-interactive content, does not receive focus, is associated with its trigger through aria-describedby, remains available while the trigger has focus or the pointer is over the trigger or tooltip, and can be dismissed with Escape.
A tooltip is supplementary by definition. Put instructions, errors, requirements, and other essential information in persistent visible text. Hiding essential content behind hover/focus only is Critical.
Do not place error messages, legal text, security warnings, or task requirements only in a tooltip. Do not attach essential explanations only to a disabled control — a disabled native form control may not dispatch the pointer/focus events a tooltip expects:
<p id="publish-requirement">Add a page title before publishing.</p>
<button type="button" disabled aria-describedby="publish-requirement">
Publish
</button>A tooltip that only appears on hover is Serious — keyboard users cannot trigger it. Keep focus on the trigger; a tooltip never receives focus. Do not add the tooltip to the tab sequence or use arrow keys to navigate its text.
document.querySelectorAll('[data-tooltip]').forEach((container) => {
const trigger = container.querySelector('[data-tooltip-trigger]');
const tooltip = document.getElementById(trigger.dataset.tooltipId);
if (!tooltip || tooltip.getAttribute('role') !== 'tooltip') return;
let pointerWithin = false;
let triggerFocused = false;
let dismissed = false;
function updateTooltip() {
tooltip.hidden = !(!dismissed && (pointerWithin || triggerFocused));
}
container.addEventListener('pointerenter', () => { pointerWithin = true; dismissed = false; updateTooltip(); });
container.addEventListener('pointerleave', () => { pointerWithin = false; updateTooltip(); });
trigger.addEventListener('focus', () => { triggerFocused = true; dismissed = false; updateTooltip(); });
trigger.addEventListener('blur', () => { triggerFocused = false; updateTooltip(); });
trigger.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !tooltip.hidden) {
dismissed = true;
updateTooltip();
event.stopPropagation();
}
});
});Track pointer and focus state separately — closing on pointer-leave when focus still keeps the tooltip active is a common failure. If a positioning library is used, inspect its rendered markup, focus handling, portal location, collision behavior, and cleanup; a library name is not evidence of accessibility.
Incorrect ARIA on tooltips is Serious — AT users receive wrong or no supplementary information.
<span class="tooltip-container" data-tooltip>
<button type="button" aria-describedby="save-tooltip"
data-tooltip-trigger data-tooltip-id="save-tooltip">
Save
</button>
<span id="save-tooltip" role="tooltip" hidden>
Saves changes to your draft.
</span>
</span>Rules:
role="tooltip"; use a native interactive elementfor the trigger when the trigger performs an action
aria-describedby (not aria-labelledby);aria-describedby may hold a space-separated list — preserve existing help, error, or instruction references when adding a tooltip
hidden to conceal; never aria-hidden="true" on an active tooltipid, including across repeated components;render the descriptive node before focus reaches the trigger
role="tooltip" on the trigger itselftabindex on the tooltip containeraria-live, role="status", or role="alert" to a tooltiparia-expanded or aria-haspopup for an automatically-displayedtooltip — those apply to button-controlled disclosures, not tooltips
aria-label that replaces different visible tooltip text
For icon-only triggers, name the control independently — the tooltip is a description, not a replacement for the accessible name:
<span class="tooltip-container" data-tooltip>
<button type="button"
aria-label="Delete quarterly report"
aria-describedby="delete-tooltip"
data-tooltip-trigger data-tooltip-id="delete-tooltip">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path d="M5 7h14M9 7V4h6v3m-8 0 1 13h8l1-13"/>
</svg>
</button>
<span id="delete-tooltip" role="tooltip" hidden>
Permanently deletes the report.
</span>
</span>Avoid repeating exactly the same words as both name and description.
When a user activates a button to show content, use a disclosure pattern instead — do not give the content role="tooltip" and do not add a live region merely because it became visible:
<button type="button" id="password-help-button"
aria-expanded="false" aria-controls="password-help">
Password requirements
</button>
<div id="password-help" hidden>
Use at least 12 characters. Include a number and a symbol.
</div>The expanded state communicates the change; focus remains on the button. If the popup contains interactive controls or must manage focus, use an appropriate dialog or popup pattern.
| Requirement | Description | Severity if failing |
|---|---|---|
| Dismissible | Escape dismisses without moving pointer or focus | Serious |
| Hoverable | User can move pointer over tooltip without it disappearing | Moderate |
| Persistent | Stays visible until trigger/tooltip loses hover or focus, is dismissed, or the info is no longer valid | Moderate |
Do not invent an auto-close duration — a tooltip that disappears after a fixed timeout does not meet the persistence requirement while its trigger remains active.
Touch devices do not provide dependable hover. Do not depend on a tooltip for touch access. Use one of:
title AttributeBrowser-controlled title tooltips fall under the WCAG 1.4.13 exception when their presentation is entirely user-agent controlled, but that exception does not make title a dependable interface pattern. Authors cannot ensure consistent keyboard, touch, magnification, timing, text size, or dismissal behavior for title. Do not use title as the only:
(WCAG 1.4.3); large text needs 3:1
1.4.11) only when that visual information is required to identify the component or state — a decorative arrow is not automatically subject to this
allow text to wrap, avoid horizontal scrolling, don't cover the trigger or critical content, and preserve a way to dismiss it
resize, content changes, and text-direction changes; avoid rendering inside an ancestor whose overflow clips the tooltip
prefers-reduced-motion:[role="tooltip"] {
position: absolute;
max-inline-size: min(20rem, calc(100vw - 2rem));
overflow-wrap: anywhere;
}
[role="tooltip"][hidden] { display: none; }
@media (forced-colors: active) {
[role="tooltip"] { color: CanvasText; background: Canvas; border-color: CanvasText; }
}
@media (prefers-reduced-motion: no-preference) {
[role="tooltip"] { transition: opacity 0.15s ease; }
}introduce an unrelated message
has focus
aria-details, a disclosure, or a dialog instead
stops make keyboard navigation harder
appears without moving focus, confirm Escape closes it while focus stays on the trigger, confirm no tooltip tab stop exists, confirm the focus indicator remains visible
part of the tooltip and confirm it stays visible, confirm it closes when neither hover nor focus retains it, test with screen magnification
content; confirm essential information is visible or reachable via an explicit control; test screen reader gestures and confirm long press is not required
the tooltip, the tooltip text is exposed as a description, name/description aren't needlessly duplicated, focus is not moved, and repeated focus doesn't produce stale descriptions. Test supported browser/AT combinations and record versions — presentation varies and no single announcement is universal
overrides, forced-colors mode, reduced motion, RTL text, viewport-edge placement
aria-describedbyreferences, invalid ARIA values, focusable descendants inside a tooltip, inadequate contrast, hidden triggers, and stale open tooltips in component tests — but cannot prove hoverability, persistence, positioning, or screen-reader presentation. Keep manual tests in the release process.
| Failure | Correction |
|---|---|
| Showing the tooltip only on hover | Show it on keyboard focus too |
| Hiding it when the pointer leaves the trigger | Keep it open while the pointer is over the trigger or tooltip |
| Hiding it when pointer leaves even though focus remains | Track pointer and focus state separately |
| Closing it after a fixed timeout | Keep it visible until trigger removal, dismissal, or invalidation |
| Moving focus into the tooltip | Keep focus on the trigger |
Putting a link or button in role="tooltip" | Use a disclosure, non-modal dialog, or other popup |
Using aria-expanded/aria-haspopup for an automatic tooltip | Use aria-describedby and role="tooltip" |
Relying on title for a control label or instruction | Provide a visible label and persistent/authored help |
Overwriting existing aria-describedby IDs | Append and preserve all relevant references |
| Attaching essential help only to a disabled control | Make the explanation visible |
role="tooltip" on tooltip element with a unique idaria-describedby without losing other descriptionsEscape dismisses without moving focus; not closed on a fixed timerprefers-reduced-motion respected for animationsStandards horizon: WCAG 3.0 is in development; 1.4.13 requirements are expected to carry forward. Monitor: <https://www.w3.org/TR/wcag-3.0/>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.