RatedWithAI

RatedWithAI

Accessibility scanner

Does your skip link actually work?

Enter any page. We find the skip-to-content link, follow it, and tell you whether a keyboard can reach it and whether it lands anywhere — then count how many times someone using only the Tab key presses it before they reach your first word.

Free, instant, no signup and no card. Nothing is stored.

Free, instant, no signup and no card. We read the HTML your server sends for that one page, plus the CSS rules inside it. Nothing is stored.

The criterion that passes every test you already run

SC 2.4.1 Bypass Blocks is level A, and it is the one failure on the list that is invisible to everybody who checks the site the normal way. A mouse user never meets it. A screen reader user routes around it with landmark navigation and may never notice. Your linter does not warn, because the markup is valid. Your build does not fail, because nothing is broken in the sense a build understands. Nothing in your CI presses Tab.

The person it strands is the one nobody pictures: sighted, not using a screen reader, navigating with the keyboard because a mouse hurts or is not available — RSI, a tremor, a broken wrist, a trackpad that died, or simply preference. For them your header is not decoration, it is a toll gate. Logo, seven nav items, a mega-menu that opens on focus, a locale switcher, a search box, a sign-in and a call to action: forty presses of the Tab key before the first sentence of the article. On the next page, forty again.

The fix has been one line of HTML since 1997. Which is exactly why the interesting result from this checker is almost never “you do not have a skip link”. It is that you do, and it stopped working, and nothing told you.

Three silent ways a skip link dies

The target got renamed. The link says href="#content" and the element it was written against is gone — a <div id="content"> became a <main> in a redesign, or the id moved into a component that no longer renders it. The browser does nothing. No console message, no 404, no visual difference. The link is now a button that scrolls to where you already are.

The CSS took it out of the tab order. This one catches careful teams, because the intent is right. You want the link invisible until it is focused, so you write display: none and then a :focus rule to bring it back. But display:none removes the element from the box tree, an element that is not rendered cannot take focus, and a rule that fires on focus therefore never fires at all. The link is in your HTML, it looks correct in every review, and it is reachable by nobody.

Something marked it hidden. aria-hidden="true" on a focusable element is the combination ARIA explicitly forbids — a screen reader user can still Tab onto it and arrive at nothing announced. tabindex="-1" on the link itself means script-only focus, on the one element whose whole job is to be first in the tab order. The hidden attribute is display:none wearing a different hat.

What this checker refuses to call a failure

This is the one checker on the site whose central judgement is a CSS judgement, and the correct technique and the broken one look identical in a browser. So the negatives are pinned as hard as the positives, by scripts/check-skip-link-audit.mjs in the build:

/* correct — rendered, off screen, focusable */
.skip-link { position: absolute; left: -9999px; }
.skip-link:focus { position: static; }
/* broken — not rendered, not focusable, :focus never fires */
.skip-link { display: none; }
.skip-link:focus { display: block; }

Off-screen positioning, clip, clip-path and negative text-indent are never reported. A display:none inside @media print is stripped before the CSS is read. An in-page anchor to #pricing is a table of contents, not a bypass mechanism, and is never counted as one. A page with nothing to bypass is never asked for a skip link. A page whose only mechanism is a <main> landmark is reported for review, because landmarks are a sufficient technique in WCAG’s own list — and then told, plainly, that they only serve half the audience.

And the honest gap: if the link’s styling lives in an external stylesheet, we cannot see it, so the result says so and gives you the manual check instead of guessing. That is the place this defect hides most often, and a checker that quietly called it a pass would be worse than no checker.

  • The skip link points at nothing

    Fails

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    The link names a fragment that does not exist anywhere on this page, so pressing it moves focus nowhere and the next Tab carries on from the top of the header. This is the most common way a skip link dies: the id it was written against was renamed in a template refactor, and nothing anywhere errors when that happens.

  • The skip link is hidden from the keyboard

    Fails

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    A rule on this page sets display:none or visibility:hidden on the skip link. Both remove the element from the tab order entirely, so the :focus rule written to reveal it can never fire — the element cannot be focused, so it never gets focus. The link is in the HTML, renders in no browser, and is reachable by nobody.

  • The skip link goes to "#"

    Fails

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    href="#" (or an empty href) is a placeholder, not a destination. It scrolls to the top of the document, which is where the keyboard user already is.

  • The skip link is out of the tab order

    Fails

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    tabindex="-1" makes an element focusable by script only. On a skip link — whose entire purpose is to be the first thing Tab reaches — it means the keyboard can never get to it.

  • The skip link is aria-hidden

    Fails

    WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A)

    aria-hidden="true" on a focusable element is the one combination the ARIA specification explicitly forbids: a screen reader user can still Tab onto it, and when they land there is nothing to announce. On a skip link it removes the mechanism from the audience most likely to use it.

  • The skip link carries the hidden attribute

    Fails

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    The HTML `hidden` attribute is display:none by another name. The element is out of the tab order and no CSS :focus rule can bring it back.

  • No way to skip the header

    Fails

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    There is no skip link in the HTML and no main landmark either, and a keyboard has to pass several focusable elements before it reaches the content. That is the exact case the criterion was written for: the same header on every page, and no mechanism to get past it.

  • The only way past the header is a landmark

    Review

    WCAG 2.1 SC 2.4.1 Bypass Blocks (Level A)

    There is no skip link, but the page does declare a main landmark, and landmark navigation is a sufficient technique for this criterion in WCAG's own list (ARIA11). So this conforms — for a screen reader user. A sighted keyboard user has no landmark shortcut and still tabs through the whole header on every page.

  • The skip link is not the first thing Tab reaches

    Review

    Focusable elements come before the skip link in the source, so a keyboard user meets those first — and if one of them is the nav itself, the link has already lost most of its value. The criterion says the mechanism comes at the beginning.

  • The target cannot take focus

    Review

    The skip link points at a real element, but that element is a <div>, <section> or <main> with no tabindex. Current browsers move focus to a fragment target regardless, so this works today; older browsers scroll the page without moving focus, which means the next Tab goes back to the header. tabindex="-1" on the target closes that gap and costs nothing.

  • The target is not the main content

    Review

    The link lands on an element that is not the page's main region and does not contain it, so the reader may be skipping into the middle of the header rather than past it. Worth one look at where focus actually goes.

  • We cannot see how the skip link is styled

    Verify

    The link's styling lives in an external stylesheet, which is not in the HTML this check reads. That matters here more than anywhere else, because the single most common skip-link defect IS a CSS rule: display:none instead of off-screen positioning. The markup is fine; the CSS is the half we cannot see.

The fix, in full

Three files, and none of the changes is bigger than a line.

<body>
  <a class="skip-link" href="#main">Skip to main content</a>
  <header>…logo, nav, search, sign in…</header>

  <main id="main" tabindex="-1">
    <h1>The first thing they came for</h1>
  </main>
</body>
.skip-link {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.skip-link:focus {
  position: fixed;
  left: 1rem;
  top: 1rem;
  width: auto;
  height: auto;
  padding: 0.75rem 1rem;
  background: #fff;
  color: #000;
  z-index: 9999;
}

The anchor goes first inside <body>, before the header, so it is the first thing Tab reaches. The id goes on the content container, and the tabindex="-1" next to it is what makes focus land there rather than just scrolling the page. The CSS keeps the element rendered the entire time — that is the whole trick, and it is the difference between a link that works and one that cannot be reached.

Then check the place it actually breaks: the target id lives in the page template, not the layout, so a blog post, a checkout and a docs page can each have a different one. A skip link that works on the home page and dies on every article is the single most common shape of this defect.

Common questions

What is a skip link?
One anchor, at the very top of the page, that jumps past the header and navigation straight to the content — "Skip to main content". It is normally invisible until it receives keyboard focus, which is why most people who own a site have never seen theirs. It exists for the person who navigates with the Tab key instead of a mouse, and it is the mechanism WCAG 2.1 SC 2.4.1 Bypass Blocks asks for at level A.
Why does a skip link break so quietly?
Because every way it breaks is valid HTML and valid CSS. The link still renders. Nothing errors, nothing logs, no build step fails, and no automated test in your pipeline is pressing Tab. The most common death is a template refactor: the link says href="#content", somebody replaced <div id="content"> with <main>, and now the link scrolls nowhere. The second most common is a stylesheet: display:none instead of off-screen positioning. Both leave a site that looks completely finished.
Why is display:none on a skip link a failure?
Because display:none removes the element from the box tree, and an element that is not in the box tree cannot receive focus. So the :focus rule written on the very next line — the one meant to reveal the link when the keyboard reaches it — can never fire, because the keyboard can never reach it. The intent was right and the result is a bypass mechanism that nobody, on any device, can ever use. visibility:hidden does the same thing. The correct technique keeps the element rendered and pushes it off screen: position:absolute; left:-9999px, or a clip-path, both reversed on :focus.
Do I need a skip link if I have a <main> landmark?
WCAG says you can conform without one: ARIA11, using landmark regions, is a sufficient technique for SC 2.4.1 in the official list. But read what that actually buys. Landmark navigation is a screen reader feature. The sighted keyboard user — RSI, tremor, a broken wrist, a power user — has no landmark shortcut and no screen reader, and still presses Tab through your entire header on every page. So this checker calls a landmark-only page REVIEW rather than a failure, and then says plainly that you should add the link anyway. It is one anchor and two CSS rules.
How many Tab presses is too many?
There is no number in the standard, which is why this checker reports the count instead of judging it. The criterion asks for a mechanism to bypass blocks of content that REPEAT across pages — so the question is not really how many, it is how many times per session. A header with thirty focusable elements is thirty presses on the home page, thirty more on the pricing page, thirty more on every blog post. Below about five there is nothing that reads as a repeated block, and this tool does not ask you for a skip link at all.
Can you see my CSS?
Only the rules inside the HTML — <style> blocks and style attributes. An external stylesheet is a separate file we do not fetch, and on this particular criterion that matters more than anywhere else, because the defect usually IS a CSS rule. So when a link's styling lives somewhere we cannot see, the result says VERIFY and gives you the ten-second manual check rather than guessing. Rules inside @media print are stripped before anything is read — hiding a skip link from the printer is correct.
What is the ten-second manual check?
Load your page. Click the browser's address bar. Press Tab once. Something visible should appear at the top-left of the page saying roughly "Skip to main content". Press Enter, then press Tab again — focus should now be inside your content, not back at the top of your navigation. If nothing appeared on the first Tab, the link is hidden from the keyboard. If the second Tab put you back in the header, the target is not taking focus.
Does my target need tabindex="-1"?
It is the belt-and-braces fix, and it costs nothing. Fragment navigation moves focus to the target in every current browser, so a <main id="main"> with no tabindex works today. Older browsers scroll the page without moving focus, which means the visual jump happens but the next Tab starts again from the top of the header — the worst possible outcome, because it looks like it worked. tabindex="-1" makes an element programmatically focusable without putting it in the tab order. This checker reports its absence for review, never as a failure.
Is a table-of-contents link a skip link?
No, and this checker will not count one. An in-page anchor to #pricing or #faq is navigation within content; a skip link bypasses the repeated block at the top of every page. We match on what the link says — "skip to", "jump to content", "skip navigation" — and, for the icon-only shape, on a fragment that names the content region on a short link. An ordinary anchor is never read as a bypass mechanism in either direction.
My header is built by JavaScript. Will this see it?
Only what is in the HTML your server sends, which is what this reads — deliberately, because it is fast, free and the same markup a crawler sees. If the checker finds no skip link on a page you know has one, that is itself worth knowing: it arrives late, so it is not the first thing the keyboard reaches. The free WCAG 2.1 AA scan renders each page in a real browser and will see it.
Does this store my domain?
No. The check is an HTTP GET of the page you name, run inside the request and discarded when it answers. There is no account, no email field and no row written anywhere. If you go on to run the free scan, that one does keep a report so you can come back to it, which is the whole point of a report.

Keep reading