Skip to content

fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable#3429

Draft
bilal-karim wants to merge 2 commits into
mainfrom
a11y/tooltip-keyboard-hover-dismiss
Draft

fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable#3429
bilal-karim wants to merge 2 commits into
mainfrom
a11y/tooltip-keyboard-hover-dismiss

Conversation

@bilal-karim
Copy link
Copy Markdown
Member

@bilal-karim bilal-karim commented May 21, 2026

Summary

  • Critical accessibility fix — the Holocene Tooltip primitive was completely inaccessible to keyboard and screen reader users
  • Addresses WCAG 2.2 SC 1.4.13 (Content on Hover or Focus), which currently scores "Does Not Support"
  • Cross-cutting: also advances SC 2.1.1 (Keyboard), SC 4.1.2 (Name, Role, Value), and unblocks fixes for SC 1.3.2 and SC 1.4.1

What was broken

The Tooltip only responded to mouseenter/mouseleave — keyboard users never saw tooltip content, there was no Escape-to-dismiss, and portal tooltips disappeared when moving the pointer toward the popover.

What this PR adds

  • Keyboard focus support — tooltips appear when any focusable child receives focus (focusin/focusout on the wrapper)
  • Escape to dismiss — pressing Escape hides the tooltip without moving focus; auto-resets when the interaction ends
  • Hoverable popover — pointer can move onto the popover content without it disappearing (diagonal hover bridge)
  • ARIA linkagerole="tooltip" + unique id on the popover, aria-describedby on the wrapper so screen readers announce tooltip content
  • Unified open state — both portal and inline variants now use a single isOpen derived from hover, focus, popover-hover, and dismiss signals

No API changes

All existing consumers work identically. The only observable difference is that tooltips now appear on keyboard focus.

Test plan

Keyboard:

  • Tab through workflow detail action bar buttons — confirm each shows its tooltip on focus
  • With a tooltip visible, press Escape — confirm it dismisses without focus moving
  • Tab away and back — confirm tooltip reappears (dismiss state cleared)

Pointer:

  • Hover a tooltip trigger, then move pointer onto the popover content — confirm it stays open
  • Move pointer off both trigger and popover — confirm it closes
  • With popover visible via hover, press Escape — confirm dismissal

Screen reader:

  • VoiceOver/NVDA: focus a tooltipped button — confirm both the button name and tooltip text are announced
  • Verify portal-based tooltips (usePortal consumers) announce correctly

🤖 Generated with Claude Code

The Tooltip primitive was mouse-only: no focus handlers, no Escape
dismiss, and portal tooltips disappeared when moving the pointer to
the popover content. This failed all three WCAG 2.2 SC 1.4.13
criteria (Content on Hover or Focus).

Changes:
- Show tooltip on keyboard focus via focusin/focusout on wrapper
- Dismiss on Escape (resets when interaction ends)
- Track pointer hover on the popover itself (diagonal hover bridge)
- Unify both portal and inline variants on a single isOpen derived
- Add role="tooltip" and aria-describedby linkage for screen readers

No API changes — all existing consumers work identically, with the
added benefit that tooltips now appear on keyboard focus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bilal-karim bilal-karim requested a review from a team as a code owner May 21, 2026 20:34
@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment May 22, 2026 12:15am

Request Review

@temporal-cicd
Copy link
Copy Markdown
Contributor

temporal-cicd Bot commented May 21, 2026

Warnings
⚠️

📊 Strict Mode: 1 error in 1 file (0.1% of 914 total)

src/lib/holocene/tooltip.svelte (1)
  • L59:13: Type 'null' is not assignable to type '"search" | "link" | "success" | "error" | "action" | "activity" | "add-square" | "add" | "apple" | "archives" | "arrow-down" | "arrow-left" | "arrow-up" | "arrow-right" | "ascending" | ... 140 more ... | "xmark-square"'.

Generated by 🚫 dangerJS against dda85de

- Use <svelte:window on:keydown> instead of reactive document
  listener (mirrors maximizable.svelte)
- Fix hover state lingering when mouse leaves popover to empty
  space by unifying wrapper + popover into a single hover zone
- Extract HOVER_HIDE_DELAY_MS constant
- Drop redundant isPopoverHovered state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bilal-karim bilal-karim changed the title fix: make Tooltip keyboard-accessible, dismissible, and hoverable fix(a11y): make Tooltip keyboard-accessible, dismissible, and hoverable May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant