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
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.
tw:sr-only —
it must stay in the tab order, and a 1px clip-rect box cannot be reliably un-hidden on focus.
tabindex="-1".skip-link-targetoutline: 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>
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.
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.
| Step | Expected |
|---|---|
| Load the page, press Tab once | document.activeElement is the .skip-link |
| Look at the top-left | Navy chip, legible white label, visible focus halo, inside the viewport |
| Press Enter | Focus is on the <main>, and no ring is painted around it |
| Press Tab again | Focus is on the first control inside <main> — the chrome is genuinely skipped |
| Tab past the link without activating it | The chip parks itself off-screen again |
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.
On a real consumer page this is the results list, the application form or the school profile — whatever the visitor actually came for.
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.