$npx -y skills add mgifford/accessibility-skills --skill tooltipsLoad this skill whenever the project contains tooltip components, hover- triggered informational popups, title attribute tooltips, or any content revealed on hover or focus. Under no circumstances create tooltips that are only triggered by hover without keyboard equivalent access
| 1 | # Tooltips Accessibility Skill |
| 2 | |
| 3 | > **Canonical source**: `examples/TOOLTIP_ACCESSIBILITY_BEST_PRACTICES.md` in `mgifford/ACCESSIBILITY.md` |
| 4 | > This skill is derived from that file. When in doubt, the example is authoritative. |
| 5 | |
| 6 | Apply these rules when implementing or reviewing tooltip components. |
| 7 | **Only load this skill if the project contains tooltips.** |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Core Mandate |
| 12 | |
| 13 | A tooltip is a short, non-interactive description that appears automatically |
| 14 | when its trigger receives keyboard focus or pointer hover. An accessible |
| 15 | tooltip must be available to keyboard, pointer, touch, magnification, and |
| 16 | assistive technology users without hiding essential information or blocking |
| 17 | the interface. |
| 18 | |
| 19 | Tooltips are often the wrong solution. Persistent visible text, a clear control |
| 20 | label, a disclosure, or a dialog is usually easier to discover and use. This |
| 21 | guide covers authored tooltips; the HTML `title` attribute produces a |
| 22 | browser-controlled tooltip with different behavior and limitations (see below). |
| 23 | |
| 24 | Use the project's `skills/bug-reporting/SKILL.md` severity/priority framework to |
| 25 | grade findings. The severity scale below is a skill-local shorthand, not a |
| 26 | universal standard. |
| 27 | |
| 28 | --- |
| 29 | |
| 30 | ## Severity Scale (this skill) |
| 31 | |
| 32 | | Level | Meaning | |
| 33 | | --- | --- | |
| 34 | | **Critical** | Tooltip contains essential information with no other access route | |
| 35 | | **Serious** | Tooltip unreachable by keyboard or AT; interactive content trapped in a tooltip | |
| 36 | | **Moderate** | Tooltip accessible but WCAG 1.4.13 requirements not fully met | |
| 37 | | **Minor** | Best-practice gap; marginal impact | |
| 38 | |
| 39 | --- |
| 40 | |
| 41 | ## Choose the Right Pattern First |
| 42 | |
| 43 | Calling every small popup a tooltip leads to incorrect semantics and keyboard |
| 44 | behavior. Classify the interaction before choosing ARIA: |
| 45 | |
| 46 | | Need | Use | |
| 47 | |---|---| |
| 48 | | A control needs a clearer label | Improve its visible label | |
| 49 | | Short supplementary text should appear automatically on focus or hover | Tooltip | |
| 50 | | Instructions or requirements are needed to complete a task | Persistent visible help text | |
| 51 | | A user explicitly asks to show or hide short help | Button-controlled disclosure | |
| 52 | | The popup contains links, buttons, or other controls | Non-modal dialog or another appropriate popup pattern | |
| 53 | | A decision is required before work can continue | Modal dialog or alert dialog | |
| 54 | | An action completed or a process changed state | Status message, not a tooltip | |
| 55 | | Text is truncated only because of layout | Allow wrapping, expansion, or another way to read the full text | |
| 56 | |
| 57 | An authored tooltip: describes an element, appears automatically after focus or |
| 58 | hover, contains only non-interactive content, does not receive focus, is |
| 59 | associated with its trigger through `aria-describedby`, remains available while |
| 60 | the trigger has focus or the pointer is over the trigger or tooltip, and can be |
| 61 | dismissed with `Escape`. |
| 62 | |
| 63 | --- |
| 64 | |
| 65 | ## Critical: Essential Information Must Not Live Only in a Tooltip |
| 66 | |
| 67 | A tooltip is supplementary by definition. Put instructions, errors, |
| 68 | requirements, and other essential information in persistent visible text. |
| 69 | **Hiding essential content behind hover/focus only is Critical.** |
| 70 | |
| 71 | Do not place error messages, legal text, security warnings, or task |
| 72 | requirements only in a tooltip. Do not attach essential explanations only to a |
| 73 | disabled control — a disabled native form control may not dispatch the |
| 74 | pointer/focus events a tooltip expects: |
| 75 | |
| 76 | ```html |
| 77 | <p id="publish-requirement">Add a page title before publishing.</p> |
| 78 | <button type="button" disabled aria-describedby="publish-requirement"> |
| 79 | Publish |
| 80 | </button> |
| 81 | ``` |
| 82 | |
| 83 | --- |
| 84 | |
| 85 | ## Serious: Tooltips Must Appear on Both Hover and Focus |
| 86 | |
| 87 | A tooltip that only appears on hover is **Serious** — keyboard users cannot |
| 88 | trigger it. Keep focus on the trigger; a tooltip never receives focus. Do not |
| 89 | add the tooltip to the tab sequence or use arrow keys to navigate its text. |
| 90 | |
| 91 | ```js |
| 92 | document.querySelectorAll('[data-tooltip]').forEach((container) => { |
| 93 | const trigger = container.querySelector('[data-tooltip-trigger]'); |
| 94 | const tooltip = document.getElementById(trigger.dataset.tooltipId); |
| 95 | if (!tooltip || tooltip.getAttribute('role') !== 'tooltip') return; |
| 96 | |
| 97 | let pointerWithin = false; |
| 98 | let triggerFocused = false; |
| 99 | let dismissed = false; |
| 100 | |
| 101 | function updateTooltip() { |
| 102 | tooltip.hidden = !(!dismissed && (pointerWithin || triggerFocused)); |
| 103 | } |
| 104 | |
| 105 | container.addEventListener('pointerenter', () => { pointerWithin = true; dismissed = false; updateTooltip(); }); |
| 106 | container.addEventListener('pointerleave', () => { pointerWithin = false; updateTooltip(); }); |
| 107 | trigger.addEventLis |