Skip to content

Accessibility: keep keyboard focus inside the mobile navigation modal #115

Description

@jinhongliang991013

Summary

The mobile navigation visually covers the page, but keyboard focus is not
contained inside it. After tabbing through the menu controls, focus moves to
links and controls in the obscured landing-page content behind the overlay.

This makes the navigation behave like a non-modal layer even though it blocks
the viewport. It becomes especially problematic if the overlay is given
role="dialog" and aria-modal="true": the accessibility semantics would say
the background is unavailable while the browser can still focus it.

Reproduction

Tested on June 12, 2026 against https://gib.work/ using Microsoft
Edge/Chromium at a 390 x 844 viewport.

  1. Open the landing page at mobile width.
  2. Activate the hamburger menu.
  3. Press Tab repeatedly without closing the menu.
  4. Observe the focus sequence after the final social link in the overlay.

Recorded focus trace

A Playwright run confirmed that the overlay remained visible throughout the
test:

Tab position Focused control Inside overlay
1-9 Home, Open App, Close, About, Testimonial, FAQ, YouTube, Discord, X Yes
10 Hero Get Started For Free link No
11 Solana link No
12+ Landing-page tabs and testimonial links No

The first background element is therefore reachable while the full-screen menu
is still open.

mobile menu open
  -> ...
  -> X social link (last overlay control)
  -> Get Started For Free (background)
  -> Solana (background)
  -> Looking for Help tab (background)

Root cause

components/nav.tsx renders the full-screen menu as an animated sibling
overlay, but it does not:

  • cycle Tab from the last menu control to the first;
  • cycle Shift+Tab from the first menu control to the last; or
  • make the page content outside the menu inert while the menu is open.

PR #113 improves the same component by adding dialog semantics, initial focus,
Escape handling, focus restoration, and scroll locking. Those changes are
useful, but its current patch does not add focus containment or inert, so this
specific failure remains after that PR.

Recommended implementation

Use one of these approaches:

  1. Render the mobile navigation with a tested modal-dialog primitive that
    provides focus containment and background inertness; or
  2. Keep the current component and implement all of the following together:
    • collect the enabled, visible focusable controls inside the overlay;
    • wrap Tab from the last control to the first;
    • wrap Shift+Tab from the first control to the last;
    • mark the background application content inert for the lifetime of the
      overlay and restore its previous state on close;
    • preserve the initial-focus, Escape-to-close, and trigger-focus-restoration
      behavior proposed in Improve landing page accessibility and mobile navigation #113.

The WAI-ARIA modal dialog pattern requires Tab and Shift+Tab to remain
inside the dialog:
https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/

The native inert attribute removes background descendants from sequential
focus and interaction:
https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert

Acceptance criteria

  • With the mobile menu open, repeated Tab cycles from the final menu
    control back to the first menu control.
  • Shift+Tab on the first menu control cycles to the final menu control.
  • No background link, button, tab, iframe, or embedded tweet control can
    receive keyboard focus while the menu is open.
  • Escape closes the menu.
  • Closing the menu returns focus to the hamburger trigger.
  • The focus restriction is removed when the menu closes.
  • Automated browser coverage verifies forward and reverse focus wrapping
    at a mobile viewport.

This is a behavioral accessibility fix only; no visual redesign is required.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions