SKIP LINK: Keyboard bypass for repeated page chrome (WCAG 2.4.1, Level A)
COMPOSES: nothing — own classes
CLASSES: .skip-link | .skip-link-target

Without one, a keyboard or switch user landing on a consumer page tabs through the header logo, the auth CTAs, the sticky four-vertical section nav, every hero-search segment and the whole filter rail before reaching a single result. The link is parked outside the viewport until it takes focus, then slides in at the top-left and jumps straight to the page's main region.

This one is live — press Tab to see it

Click anywhere in this preview, then press Tab until focus reaches the top of the document. A navy “Skip to main content” chip appears at the top-left. Press Enter and focus lands on the “Main content starts here” region below; the next Tab goes to the first control inside it, not back to the top. Automated checkers cannot test any of this — see “Why the a11y gate can’t catch this”.

Anatomy — two halves of one contract

The link and its target ship together. A bypass link pointing at an id that doesn't exist is a dead control that passes inspection and fails the user, so .skip-link is opt-in per page: the shared top frame renders it only when the page passes skip_to:, and the page adds the matching id, tabindex="-1" and .skip-link-target to its <main> in the same edit.

1. The link
First focusable element in the DOM. Hidden by transform, not by tw:sr-only — it must stay in the tab order, and a 1px clip-rect box cannot be reliably un-hidden on focus.
2. tabindex="-1"
Makes a non-interactive landmark focusable. Without it, some browsers scroll to the anchor but leave focus in the header, so the next Tab drops the user back into the nav they just skipped.
3. .skip-link-target
Suppresses the browser's own outline: auto, which otherwise paints a ring around the entire main region — a full-viewport blue frame that reads as a rendering fault.
<a class="skip-link" href="#main-content">Skip to main content</a>
<main id="main-content" tabindex="-1" class="skip-link-target">…</main>

Wiring a page

Two lines per page. shared/_consumer_topframe_session hosts the link because it is already the first thing every consumer preview renders. Pass skip_to:, then mark up the target.

<%= render "shared/consumer_topframe_session", active: :providers, logged_in: false, skip_to: "#main-content" %>
<main id="main-content" tabindex="-1" class="skip-link-target tw:min-h-screen tw:bg-white">

Omit skip_to: and no link renders — the safe default for a page whose <main> has not been given an id yet.

Why the a11y gate can't catch this

Manual verification is the only verification. axe has no rule for SC 2.4.1 — no automated checker can tell whether a bypass mechanism exists, because deciding what counts as “repeated blocks” needs a human reading the page. A green a11y run on a consumer page says nothing at all about this criterion.

StepExpected
Load the page, press Tab oncedocument.activeElement is the .skip-link
Look at the top-leftNavy chip, legible white label, visible focus halo, inside the viewport
Press EnterFocus is on the <main>, and no ring is painted around it
Press Tab againFocus is on the first control inside <main> — the chrome is genuinely skipped
Tab past the link without activating itThe chip parks itself off-screen again

Live target

Target: the region below stands in for a page's <main>. It carries id="preview-main", tabindex="-1" and .skip-link-target — the same three things a real page adds.

More than one skip target

No extra class needed. A search page where the filter rail sits between the nav and the results can justify a second bypass ("Skip to results"). Two sibling .skip-link elements need nothing new: both park off-screen, and because only one can hold focus at a time they take turns occupying the same top-left slot as Tab moves through them. Each still needs its own id + tabindex="-1" + .skip-link-target landing zone.

<a class="skip-link" href="#main-content">Skip to main content</a>
<a class="skip-link" href="#results">Skip to results</a>

Keep the list short. Every skip link is itself a tab stop, so three or four of them recreate the problem they exist to solve.