a11y: add keyboard focus-trap to shared Modal (Tab cycling within dialog) #1

Open
opened 2026-07-11 19:39:25 +00:00 by Somehuman · 0 comments
Owner

Background

While shipping the venue-page "Request to book" hero button (opens the booking-request form in a dialog), we hardened the shared Modal's accessibility. The following landed in packages/ui/src/ui/tamagui/components/overlay/Modal.tsx:

  • role="dialog" + aria-modal="true" on the panel container
  • aria-labelledby tied to the Modal header title (guarded so header-less consumers don't emit a dangling reference)
  • Initial focus moved into the panel on open (with a "don't steal focus from a consumer that focuses its own heading/input" guard)
  • Focus returned to the opening trigger on close

What's deferred (this issue)

A full keyboard focus-trap — constraining Tab / Shift+Tab to cycle within the open dialog instead of moving into the background page — was intentionally deferred. aria-modal="true" means assistive tech already treats the background as inert, but the DOM tab order is not yet constrained, so a sighted keyboard user can Tab out of the dialog into the page behind it.

It was deferred because Modal is a shared primitive with 30+ consumers (auth overlays, admin gates, broadcast composer, claim wizard, ask/manage views, etc.), and a naive trap risks regressing consumers that manage their own focus. It needs its own scoped pass.

Scope / acceptance criteria

  • Tab and Shift+Tab cycle focus among the focusable elements inside the open Modal panel; focus cannot reach background page content while a modal is open.
  • Respect nested/stacked modals (only the topmost traps).
  • Do not regress existing consumers that set their own initial focus (e.g. ClaimVenueWizard focuses its heading; AdminEmailVerificationGate is header-less).
  • Preserve existing Esc / backdrop-click / body-scroll-lock behavior.
  • Honor prefers-reduced-motion (no change expected, just don't regress).
  • Add/extend overlay tests to cover trap + focus-return.

Pointers

  • packages/ui/src/ui/tamagui/components/overlay/Modal.tsx — the component; see the recently added role/aria-modal/aria-labelledby/focus-in/focus-return logic to build on.
  • Existing overlay tests: OverlayHost, AppOverlayProvider, SignInOverlay, UnifiedAuthOverlay, BroadcastComposer, AskManageView.

Priority: low/medium (a11y polish; aria-modal mitigates the AT case today).

## Background While shipping the venue-page "Request to book" hero button (opens the booking-request form in a dialog), we hardened the shared Modal's accessibility. The following **landed** in `packages/ui/src/ui/tamagui/components/overlay/Modal.tsx`: - `role="dialog"` + `aria-modal="true"` on the panel container - `aria-labelledby` tied to the Modal header title (guarded so header-less consumers don't emit a dangling reference) - Initial focus moved into the panel on open (with a "don't steal focus from a consumer that focuses its own heading/input" guard) - Focus returned to the opening trigger on close ## What's deferred (this issue) A **full keyboard focus-trap** — constraining Tab / Shift+Tab to cycle *within* the open dialog instead of moving into the background page — was intentionally deferred. `aria-modal="true"` means assistive tech already treats the background as inert, but the DOM tab order is not yet constrained, so a sighted keyboard user can Tab out of the dialog into the page behind it. It was deferred because `Modal` is a shared primitive with **30+ consumers** (auth overlays, admin gates, broadcast composer, claim wizard, ask/manage views, etc.), and a naive trap risks regressing consumers that manage their own focus. It needs its own scoped pass. ## Scope / acceptance criteria - Tab and Shift+Tab cycle focus among the focusable elements inside the open Modal panel; focus cannot reach background page content while a modal is open. - Respect nested/stacked modals (only the topmost traps). - Do not regress existing consumers that set their own initial focus (e.g. `ClaimVenueWizard` focuses its heading; `AdminEmailVerificationGate` is header-less). - Preserve existing Esc / backdrop-click / body-scroll-lock behavior. - Honor `prefers-reduced-motion` (no change expected, just don't regress). - Add/extend overlay tests to cover trap + focus-return. ## Pointers - `packages/ui/src/ui/tamagui/components/overlay/Modal.tsx` — the component; see the recently added `role`/`aria-modal`/`aria-labelledby`/focus-in/focus-return logic to build on. - Existing overlay tests: `OverlayHost`, `AppOverlayProvider`, `SignInOverlay`, `UnifiedAuthOverlay`, `BroadcastComposer`, `AskManageView`. Priority: low/medium (a11y polish; `aria-modal` mitigates the AT case today).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Somehuman/hearthfire#1
No description provided.