Skip to content

FIX: add a skip-to-main-content link for keyboard users - #2598

Open
Akshar Gothi (akshar27) wants to merge 4 commits into
microsoft:mainfrom
akshar27:fix/frontend-skip-link
Open

FIX: add a skip-to-main-content link for keyboard users#2598
Akshar Gothi (akshar27) wants to merge 4 commits into
microsoft:mainfrom
akshar27:fix/frontend-skip-link

Conversation

@akshar27

Copy link
Copy Markdown

Description

The application shell has no skip link or equivalent bypass control. On every route, keyboard users must Tab through the same shell controls (Take a tour, primary navigation, Feedback, Security, Theme) before reaching page content — 10 Tab presses on the Home page per the issue. This doesn't satisfy the bypass-blocks expectation in WCAG 2.4.1.

Adds a visually-hidden-until-focused skip link as the very first focusable element in MainLayout, targeting the existing main landmark referenced in the issue (MainLayout.tsx:89). Gave <main> a stable id="main-content" and tabIndex={-1} so activating the link moves keyboard focus directly into the landmark (not just the scroll position, which a plain anchor jump alone wouldn't reliably do for a non-naturally-focusable element).

The skip link reuses the existing Fluent UI brand tokens (colorBrandBackground/colorNeutralForegroundOnBrand) already used elsewhere in the app for prominent elements, so it's visually consistent with the rest of the design system when it appears on focus.

Fixes #2597

Tests and Documentation

  • Added a new test to MainLayout.test.tsx asserting: the skip link renders with href="#main-content", <main> has the matching id and tabIndex={-1}, and the skip link is the first focusable element among a[href]/button/[tabindex] in the rendered tree (this is what guarantees it's reached on the very first Tab press).
  • npm test — all 1409 tests pass (70 suites).
  • npm run lint / npm run type-check / npm run build — all clean.
  • Manually verified end-to-end in a running dev server (npm run dev, with a minimal stub for /api/auth/config returning empty client/tenant IDs to exercise the app's existing no-auth dev path): confirmed via the DOM that (1) the skip link is the first focusable element within the app root, (2) it's positioned off-screen by default (top: -40px) and only reveals itself when it receives keyboard-driven focus (Griffel's generated :focus-visible rule sets top: 0), and (3) activating it moves document.activeElement to the <main> landmark (id="main-content"), not just the scroll position. I couldn't get the CDP-synthesized Tab key to trigger Chromium's native focus traversal reliably in the browser-automation environment I have access to, so I verified the same underlying behavior directly instead — happy for a reviewer to also spot-check with a real Tab press.

Notes

  • GitHub Actions doesn't appear to be enabled yet on my fork (akshar27/PyRIT) — a one-time manual step outside this PR. I'll push a retrigger commit once it's on, if CI doesn't pick this PR up.

The application shell has no bypass control, so keyboard users must
tab through every shell control (Take a tour, primary navigation,
Feedback, Security, Theme) before reaching route content on every
page. This does not satisfy the WCAG 2.4.1 bypass-blocks requirement.

Add a visually-hidden-until-focused skip link as the first focusable
element in MainLayout, targeting the existing main landmark. Give
<main> a stable id and tabIndex={-1} so activating the link moves
keyboard focus directly into it, not just the scroll position.

Fixes microsoft#2597
@romanlutz Roman Lutz (romanlutz) self-assigned this Sep 8, 2026
Comment thread frontend/src/components/Layout/MainLayout.styles.ts Outdated
Comment thread frontend/src/components/Layout/MainLayout.test.tsx
…2e coverage)

Per @romanlutz's review:
- Hide the skip link with transform: translateY(-100%) instead of a fixed
  top: -40px offset, so it stays fully off-screen regardless of its own
  rendered height (text zoom, a different font, or longer copy could
  otherwise leave part of a taller link visible over the top bar).
- Disable the reveal transition under prefers-reduced-motion.
- Add a real e2e test (Playwright) that presses Tab, asserts the link is
  focused and visible, presses Enter, and asserts focus lands on #main-content
  - the Jest test only checked markup and would still pass if the CSS
  reveal or the focus-on-activate behavior broke.
@akshar27

Copy link
Copy Markdown
Author

Both addressed in c77392b:

  1. Switched to transform: translateY(-100%) (with top: 0 as the anchor) instead of the fixed top: -40px, so the link stays fully off-screen regardless of its own rendered height. Also added transitionDuration: '0s' under @media (prefers-reduced-motion: reduce) so it snaps instead of sliding for users who request that.
  2. Added a real Playwright e2e test in e2e/accessibility.spec.ts that presses Tab, asserts the link is focused and visible, presses Enter, and asserts #main-content receives focus. Since the shared beforeEach in that file depends on a real backend I don't have running for this test's own boot sequence, I mocked the boot-time /api/* calls the same way labels-operation-picker.spec.ts already does, so the test is self-contained and doesn't depend on backend availability. Ran it locally against a real Chromium — passes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@romanlutz

Copy link
Copy Markdown
Contributor

You'll need to accept the CLA otherwise we can't accept the contribution. Akshar Gothi (@akshar27)

@akshar27

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

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.

Frontend lacks a main-content skip link for keyboard users

2 participants