Skip to content

a11y(2.1.1): navigation primitives — replace hover-only CSS :after label tooltip with focusable Tooltip#3529

Merged
ardiewen merged 4 commits into
mainfrom
wcag/2.1.1-navigation-css-tooltip-disclosure
Jun 17, 2026
Merged

a11y(2.1.1): navigation primitives — replace hover-only CSS :after label tooltip with focusable Tooltip#3529
ardiewen merged 4 commits into
mainfrom
wcag/2.1.1-navigation-css-tooltip-disclosure

Conversation

@rosanusi

Copy link
Copy Markdown
Contributor

Description & motivation 💭

Three navigation primitives (navigation-item, navigation-button, navigation-badge) rendered their text label as a CSS :after pseudo-element keyed exclusively to :hover via group-data-[nav=closed]:hover:after:flex. In the collapsed rail, keyboard users tabbing through the icons received no visible label and no accessible description — pseudo-elements are not in the accessibility tree.

This PR replaces the :after/data-tooltip block in each component with the Tooltip primitive (right, hide={$navOpen}), which provides:

  • Label on focus — the tooltip appears when the trigger (<a> / <div role="button">) receives keyboard focus
  • aria-describedby linkage — screen readers announce the label as a description
  • Escape-to-dismiss — keyboard users can hide the tooltip without losing focus
  • Mouse parity — hover path unchanged; the Tooltip's mouseenter/mouseleave drives the same popover

hide={$navOpen} suppresses the tooltip when the nav is expanded, since the inline label is already visible in that state — matching the original group-data-[nav=closed] gating.

Depends on: the Tooltip primitive must support focusin/focusout + Escape-to-dismiss + role="tooltip" + aria-describedby (tracked in 1.4.13-tooltip.md). This PR is drafted to land after that work.

Files changed (3):

  • src/lib/holocene/navigation/navigation-item.svelte (finding Install testing #1)
  • src/lib/holocene/navigation/navigation-button.svelte (finding #2a)
  • src/lib/holocene/navigation/navigation-badge.svelte (finding Add navigation #3)

SC: 2.1.1 Keyboard (Level A) — current verdict: Fails → Supports (for these three primitives)

Screenshots (if applicable) 📸

No visual change on hover. New behaviour: tooltip appears on keyboard focus in collapsed rail state.

Design Considerations 🎨

None — the Tooltip primitive reuses existing visual styling. Layout classes controlling icon visibility in nav-open state are preserved on the Tooltip wrapper.

Testing 🧪

How was this tested 👻

  • Manual testing

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Collapse the side nav and Tab through every nav item, nav button, and nav badge in order. Confirm a tooltip appears on each focus.
  2. While a tooltip is visible, press Escape — confirm the tooltip dismisses without moving focus.
  3. Expand the nav and Tab through again. Confirm no tooltip appears (the inline label is already visible).
  4. Screen reader (VoiceOver / NVDA): on a focused collapsed nav item, confirm the announcement includes the label as a description.
  5. Hover regression: confirm the popover still appears on mouse hover for all three primitives.
  6. axe-core run on / (sidebar always present): zero new violations.

Checklists

Draft Checklist

  • Blocked on 1.4.13-tooltip.md — Tooltip primitive must have focus + Escape + aria-describedby support before merging
  • VoiceOver + NVDA smoke test on collapsed rail

Merge Checklist

  • 1.4.13-tooltip.md fix is merged and the Tooltip primitive supports keyboard focus
  • All three nav primitives tested with keyboard Tab in collapsed rail
  • Hover regression confirmed

Issue(s) closed

Closes findings #1, #2a, #3 from audit-output/issues/2.1.1-keyboard-audit.md.

Docs

Any docs updates needed?

No doc changes needed.

A11y-Audit-Ref: 2.1.1-navigation-css-tooltip-disclosure

…primitive

The CSS :after / data-tooltip pattern on the three nav primitives fired only
on :hover with no :focus companion, making labels invisible to keyboard users
in the collapsed rail. Replace all three with the Tooltip component (right,
hide={$navOpen}) so labels appear on focus, are linked via aria-describedby,
and are dismissible with Escape.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment Jun 17, 2026 9:31pm

Request Review

@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-3 Bucket 3: engineer required a11y:sc-2.1.1 labels Jun 10, 2026
@rosanusi
rosanusi marked this pull request as ready for review June 11, 2026 15:42
@rosanusi
rosanusi requested a review from a team as a code owner June 11, 2026 15:42
@ardiewen

Copy link
Copy Markdown
Contributor

@rosanusi some changes are required here.

  1. (BLOCKER) The focus of the a anchor tag in the nav item bubbles up, not down, so the Tooltip never receives focus during keyboard nav. The hierarchy needs to be adjusted to fix this, I'm investigating this currently.

  2. There's a visual regression when the nav is expanded (no space between icon and text):

Screenshot 2026-06-17 at 3 49 36 PM
  1. The tooltip styling looks different than the existing implementation. Not sure if this matters? Do we accept tooltip default styling, or do you want it to match what the old implementation looked like exactly?
Screenshot 2026-06-17 at 3 49 40 PM

…ate on data-nav

Restructure navigation-item/button/badge so <Tooltip> wraps the focusable
trigger (<a> / <div role="button">) instead of just the icon, so focusin
bubbles up to the Tooltip wrapper and the label tooltip appears on keyboard
focus in the collapsed rail. Move the icon's h-6 w-6 box into an always-
rendered <div> inside the trigger so the icon sizing/spacing survives in every
state.

Drive the tooltip's visibility off the nav's local data-nav group state instead
of the global $navOpen store: tooltipClass adds group-data-[nav=open]:hidden, so
the tooltip is suppressed whenever labels are visible (desktop-expanded AND the
mobile overlay, which hardcodes data-nav="open") and shown only in the collapsed
icon rail (data-nav=closed). Previously hide={$navOpen} coupled tooltip
visibility to a persisted desktop preference, which leaked into mobile: a user
who collapsed the desktop nav (navOpen=false) would get redundant tooltips over
the always-labeled mobile menu. The navigation-item label's pointer-events is
likewise moved to group-data-[nav=closed]:pointer-events-none, and the now-unused
navOpen import is dropped from all three.

Tooltip box matched to the old :after via tooltipClass only (square corners for
item/button, rounded for badge; white text; ml-4 for the old ~32px icon gap);
the shared Tooltip primitive is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-css-tooltip-disclosure

# Conflicts:
#	src/lib/holocene/navigation/navigation-button.svelte
@ardiewen
ardiewen merged commit 5bf0900 into main Jun 17, 2026
18 checks passed
@ardiewen
ardiewen deleted the wcag/2.1.1-navigation-css-tooltip-disclosure branch June 17, 2026 22:08
laurakwhit added a commit that referenced this pull request Jun 23, 2026
Auto-generated version bump from 2.51.0 to 2.51.1

Specific version: 2.51.1

Changes included:
- [`c848ec54`](c848ec5) fix(a11y): announce workflow / event status on timeline graph nodes (WCAG 1.4.1) (#3443)
- [`af6b12a9`](af6b12a) fix(deployments): align version/status/compute badges in worker deployments table (#3489)
- [`df86587c`](df86587) Add SDK version to standalone activity details (#3447)
- [`59695290`](5969529) Use shared FilterBar component for workflows (#3448)
- [`ded94469`](ded9446) Set display contents in wrapper div if no afterLabel in Input component (#3513)
- [`d7e5eb43`](d7e5eb4) fix(standalone-activities): decode failure encodedAttributes in activity detail (#3507)
- [`81a255a9`](81a255a) fix(DT-4151): fix bottom nav scrollability and double-menu on mobile (#3514)
- [`368f646e`](368f646) Fix exports to work properly with vite8 (#3519)
- [`231bdaff`](231bdaf) chore(deps-dev): bump vitest from 3.2.4 to 3.2.6 (#3509)
- [`1de44ea6`](1de44ea) fix(workers): soften no-workers-polling alert for serverless worker deployments (#3524)
- [`689955a8`](689955a) fix(DT-4080): surface error when validate connection has no compute config, warn on set current (#3516)
- [`93323db9`](93323db) feat(DT-4078): Add Set Ramping Version to Worker Deployment Versions (#3508)
- [`9d3450c0`](9d3450c) fix(DT-4081): surface task queue not registered error in validate connection modal (#3517)
- [`b7f56237`](b7f5623) feat(DT-4079): miscellaneous WD UI fixes (#3511)
- [`83b2d4ec`](83b2d4e) feat(deployments): simplify create worker deployment UX (#3518)
- [`ccb4cb73`](ccb4cb7) Standalone Activity write guards (#3534)
- [`ab20ef03`](ab20ef0) Worker insights UI updates (#3520)
- [`aaf63bdc`](aaf63bd) [wcag] Replace text-[12px] with text-xs in DatePicker action buttons (SC 1.4.4) (#3525)
- [`743be902`](743be90) a11y(2.4.3): replace flex-wrap-reverse with flex-wrap on workflow history toolbar (#3527)
- [`41b73e87`](41b73e8) a11y(2.4.2): ui-main — set distinct, descriptive titles on login/signin, task-queues list, and activity-workers sub-tab (#3535)
- [`3d64cdbf`](3d64cdb) [WCAG 2.4.3] group-details-row — remove redundant outer focus stop on <g> wrapper (#3537)
- [`883e5db1`](883e5db) a11y(2.4.6): datepicker — replace empty label with translate('common.start') (#3543)
- [`9da915e2`](9da915e) a11y(2.4.5): import history — add back-link to Import landing page (#3542)
- [`ca4c1d61`](ca4c1d6) Cache new Intl.DateTimeFormat (#3565)
- [`38376e4d`](38376e4) Fix required activity timeout inputs on Start a Standalone Activity form (#3567)
- [`49d7c9e4`](49d7c9e) Use separate copy for title vs menu item (#3568)
- [`3c7dcdc3`](3c7dcdc) a11y(2.5.8): pagination — grow caret buttons from 12×12 to 24×24 CSS px (#3547)
- [`b5171825`](b517182) a11y(2.5.8): chip — pad remove button to 24×24 CSS px minimum (#3544)
- [`1fea5d5c`](1fea5d5) a11y(2.1.1): fake-button widgets — replace deprecated onkeypress with onkeydown handling Enter and Space (#3528)
- [`5bf09000`](5bf0900) a11y(2.1.1): navigation primitives — replace hover-only CSS :after label tooltip with focusable Tooltip (#3529)
- [`deed43f0`](deed43f) a11y(2.1.1): event summary row — add keyboard-accessible expand control alongside the mouse-only <tr onclick> (#3526)
- [`d401123e`](d401123) a11y(2.5.8): checkbox — add min-h-6 min-w-6 to Label for 24×24 hit area (#3545)
- [`aef4166f`](aef4166) [a11y] Expose toggled state via aria-pressed on ToggleButton (#3559)
- [`797f8831`](797f883) Show single attempt in Standalone Activity details (#3572)
- [`893bc58c`](893bc58) feat(DT-4183): Add Tabs (Primitive) (#3574)
- [`323c1993`](323c199) Date picker input field bug/kt (#3522)
- [`3ac34ce2`](3ac34ce) Various serverless UI updates (#3573)
- [`f365416a`](f365416) a11y(2.1.1): ZoomSvg — add keyboard pan/zoom controls so the workflow family tree is operable without a mouse (#3530)
- [`feb6897e`](feb6897) Fix gaps in slot cards on worker details page (#3580)
- [`b3c80072`](b3c8007) Remove font weight on cache size (#3583)
- [`0e7337ea`](0e7337e) Bump API to v1.62.14 (#3575)

Co-authored-by: laurakwhit <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y:bucket-3 Bucket 3: engineer required a11y:sc-2.1.1 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants