# Missing skip links past repeated navigation

Source: https://easeweb.dev/learn/skip-link
Topics: Keyboard and focus > A skip link reaches the main content (WCAG 2.4.1)
The fix: Give people a way past the repeated header. Either add a skip link as the first focusable element, shown on focus, that moves focus to the main element, or mark the page up with header, nav and main landmarks and a heading at the start of the content. Both pass; only the skip link helps sighted keyboard users, so most sites should have both.
Test with: keyboard, screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html https://www.w3.org/WAI/WCAG22/Techniques/general/G1 https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA11 https://www.w3.org/WAI/WCAG22/Techniques/html/H69 https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/main

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A garden centre's shop has a header with its logo, twelve category links, a search field, an account link and a basket. Someone using a keyboard opens the "Spring bulbs" page and presses Tab. Focus goes to the logo, then to each category in turn, then search, account and basket. The first product is the seventeenth stop. On the next page, the same sixteen stops come first again.

## Why it fails

The theme has a skip link, but its stylesheet hides it with `display: none` so that it never shows. That also removes it from the tab order, so nobody can use it. The content sits in a `div`, the header in another `div`, and the first heading is styled text in a `p`. There is no landmark or heading to jump to with a screen reader either. WCAG 2.4.1 asks for some mechanism to bypass blocks repeated on every page, and this page offers none.

## Who is affected

Keyboard users and switch users, who move one stop at a time, pay the cost of the header on every page. For a switch user each stop can take seconds. Screen reader users can usually jump by landmark or heading, but only if the page has them. People using screen magnifiers lose their place as focus travels across a wide header.

## What automation and AI miss

A scanner finds the skip link in the markup and may count it as present, even though `display: none` takes it out of the tab order. It rarely checks that the target id exists, or that focus really moves there rather than only scrolling. A generated fix often adds `<a href="#main">` and forgets to give any element `id="main"`. Single-page apps can break a working skip link by intercepting hash links in the router.

## Before: a skip link nobody can reach

```html
<a class="skip-link" href="#content">Skip to content</a>
<div class="header">
  <a href="/">Fernway Garden</a>
  <ul class="menu">…12 category links…</ul>
  <input type="search" aria-label="Search">
  <a href="/account">Account</a> <a href="/basket">Basket</a>
</div>
<div class="content">
  <p class="title">Spring bulbs</p>
  …
</div>
```

```css
.skip-link { display: none; }
```

`display: none` removes the link from the tab order, and `#content` matches no id. The page has no landmarks and no heading, so there is no way past the header.

## After: a skip link shown on focus

```html
<a class="skip-link" href="#main">Skip to main content</a>
<div class="header">…</div>
<main id="main" tabindex="-1">
  <h1>Spring bulbs</h1>
  …
</main>
```

```css
.skip-link {
  position: absolute;
  left: 8px;
  top: 8px;
  transform: translateY(-200%);
}
.skip-link:focus {
  transform: none;
}
```

The link is the first Tab stop and moves into view when it has focus. Its target exists, and `tabindex="-1"` lets `main` take focus, so the next Tab starts inside the content in every browser rather than back at the header. Sighted keyboard users can see it and use it.

## After: landmarks and a heading

```html
<header>
  <a href="/">Fernway Garden</a>
  <nav aria-label="Categories">
    <ul>…12 category links…</ul>
  </nav>
  <search><input type="search" aria-label="Search"></search>
  <a href="/account">Account</a> <a href="/basket">Basket</a>
</header>
<main>
  <h1>Spring bulbs</h1>
  …
</main>
```

The header, the navigation and the content are now landmarks, and the content starts with a level 1 heading. Screen readers have keys of their own for moving through a page: in NVDA and JAWS, D or R moves to the next landmark, Q to the main one, and H or 1 to the next heading; in VoiceOver, the rotor lists both. With `main` and an `h1` in place, one of those keys takes a screen reader user straight past the header. WCAG accepts this as a way to bypass blocks. Someone who uses a keyboard without a screen reader has none of those keys, so for them nothing changes: Tab still visits every link in the header.

## Which option to use

Both pass 2.4.1. The skip link is the only one that helps a sighted person on a keyboard or a switch, and it costs one link and a few lines of CSS. Landmarks and a heading cost nothing to keep, help every screen reader user and also meet 1.3.1, but leave the Tab journey as long as before. Use the skip link and add the landmarks too; if only one is possible in this release, start with the skip link.

## Implementation decisions

Hide the link with a transform, a clip or an off-screen position, never with `display: none`, `visibility: hidden` or `hidden`, and remove the hiding on `:focus`. Give it the same focus ring as other links and a contrast that passes on the header.

`tabindex="-1"` on `main` lets some browsers draw a focus ring around the whole content. That is acceptable, and it shows where focus went; if you remove it, use `main:focus:not(:focus-visible)` rather than `outline: none`. Don't make `main` a normal Tab stop with `tabindex="0"`.

A sticky header can cover the top of `main` after the jump. Add `scroll-margin-top` to the target, the height of the header. In a single-page app, check that the router does not swallow `#main` links; if it does, handle the click and call `focus()` on the target. Keep the skip link in the shared layout, so every page has it and its target.

One skip link to the main content is enough for most sites. Add more ("Skip to filters") only when another long block repeats too.

## Verify the fix

1. Load the page, press Tab once and check that the skip link is the first stop and is visible.
2. Press Enter on it, then Tab, and check that focus lands on the first link in the content, not back in the header.
3. Open a screen reader's landmark and heading lists and check that the main content and its heading are there.
4. Navigate to another page in the app and repeat the first two steps.
5. At 200% zoom, check that the skip link is readable and the target is not hidden under a sticky header.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the header or the layout changes.

## Limits

This is a starting pattern, not a guarantee for every application. Pages with several repeated blocks, iframes or app shells may need more than one way past them. Test the complete journey on your real pages.
