( Operable / WCAG 2.4.1 )

Skip link target is missing or not focusable

ModerateLevel AWCAG 2.4.1 — Bypass Blocks

What is this issue?

A link whose visible text suggests it’s a skip-navigation link (containing a word like “skip,” “jump,” or “main”) has an href="#some-id" where some-id either matches no element’s id anywhere on the page, or matches an element that isn’t a natively focusable element (a, button, input, select, textarea) and carries no tabindex="0" or tabindex="-1".

The skip link itself looks correct: it’s visible, it’s the first focusable element, its text reads “Skip to main content.” But the fix it’s supposed to deliver never actually happens, because the browser either scrolls to nothing at all or scrolls to an element that can’t accept keyboard focus.

Why does this matter?

A skip link exists to move keyboard focus, not just scroll position, past repeated navigation. When the target is missing or unfocusable, the visible page may still scroll toward the right area, or it may not move at all, but the next Tab press starts wherever focus actually was before the link was activated. Users don’t get an error message; they get a silent no-op that looks like it should have worked.

This is worse in practice than having no skip link at all, because it looks fixed. A user who’s learned that this site has a skip link tries it, gets nothing, and now has to decide whether the page failed to load correctly, whether they mis-clicked, or whether the skip link never worked in the first place, before falling back to the exact Tab-through-everything workaround the skip link was supposed to eliminate.

Who is affected?

  • Keyboard users: activate what looks like a working skip link and get no focus movement, then have to manually Tab through the entire navigation anyway, having lost time to a fix that silently failed.
  • Screen reader users: hear the skip link announced and activated normally, but the next item read is unrelated to the main content, since focus never actually moved to it.
  • Motor impairments: anyone relying on a switch device or head pointer pays the highest cost here, since a failed skip link means repeating the full, physically expensive navigation traversal the link was meant to replace.

What users experience

Devon uses a switch device to navigate the web after a spinal cord injury limits his hand mobility, and every keyboard action costs him noticeably more effort than a standard Tab press does for most users. He activates a “Skip to main content” link on a retailer’s site, expecting to land in the product grid. Nothing happens: the target id was renamed during a recent redesign and no longer matches the link’s href. He has no way to know the link is broken rather than working correctly, so he Tabs through all 24 header and navigation links anyway, the exact cost the skip link exists to remove.

How do I fix it?

Confirm the id referenced in the skip link’s href exists on a real element in the DOM, and that the element is either natively focusable or carries tabindex="-1". This works because it restores both halves of what a working skip link needs: a target the browser can actually find, and a target the browser can actually move keyboard focus to. Matching ids alone only handle scrolling, not focus.

The most common fix is placing tabindex="-1" directly on the <main> element and pointing the skip link’s href at its id. If the intended target is a <div> or <section> instead of <main>, either convert it to <main> where semantically appropriate, or keep the tabindex="-1" approach on whatever element you use: the attribute, not the tag name, is what makes it focusable.

Code Examples

Before
<a href="#main">Skip to content</a>
<!-- ... -->
<div id="content">
After
<!-- Method 1: matching id + tabindex="-1" on a non-focusable element -->
<a href="#main">Skip to content</a>
<!-- ... -->
<main id="main" tabindex="-1">

<!-- Method 2: point at a naturally focusable element instead -->
<a href="#search">Skip to search</a>
<!-- ... -->
<input id="search" type="search" aria-label="Search">

The original example fails two ways at once: the link’s href says #main but the target’s id is content, so the browser can’t find the element at all, and even a corrected id on a plain <div> still wouldn’t be focusable without tabindex="-1". Method 1 fixes both problems together. Method 2 shows that when the target is already a naturally focusable element, like an <input>, no tabindex is needed at all: its native focusability is enough.

Common Mistakes

Mistake: “The ids match, so the link must work.” A matching id only guarantees the browser scrolls the target into view; it says nothing about whether keyboard focus moves there. A <div id="main"> with no tabindex will visually scroll into place when the skip link is clicked, but the very next Tab press starts back at the top of the page, because focus never left its previous position.

Mistake: “I renamed a section’s id during a redesign; the skip link will still find it by context.” Browsers resolve href="#id" links by exact string match against the DOM’s id attribute; there’s no fallback to “the element that used to have this name” or “the element in roughly the right place.” A single renamed id silently breaks every skip link (and every other in-page anchor) pointing at it, with no error anywhere.

Mistake: “tabindex=‘0’ is safer than tabindex=‘-1’ because it’s more accessible.” For a skip link’s target, tabindex="0" is the wrong choice: it inserts the element into the page’s normal Tab order, so every keyboard user now hits an extra, unlabeled stop at that point on the page even when they never used the skip link. tabindex="-1" allows focus only when explicitly moved there by script or a fragment link, which is exactly the skip-link use case.

How RedFlag Detects This

Custom automated: RedFlag’s own DOM detector, runs on every scan. RedFlag selects every <a href="#..."> whose visible text contains “skip,” “jump,” or “main,” then resolves the fragment against the page’s ids. It flags the link if no element with that id exists anywhere in the DOM, or if one does exist but isn’t a natively focusable element (a, button, input, select, textarea) and has no tabindex="0" or tabindex="-1".

False negative: the keyword match (“skip,” “jump,” “main”) is a heuristic, not a complete definition of a skip link; a link reading “Bypass navigation” or “Go to content” with none of those three words passes this check without being evaluated at all, even if its target is equally broken. False positive: none typical, since resolving whether a fragment target exists and is focusable is a deterministic DOM check once a link is correctly identified as a skip link. Manual step: activate every candidate skip link with the keyboard and confirm focus visibly lands inside the main content region, not just that the page scrolled.

Manual Testing

  1. Open the page in Chrome or Firefox with NVDA or VoiceOver running.
  2. Tab once from the top of the page to reach the skip link, and activate it with Enter.
  3. Immediately press Tab again and note where focus lands.
  4. Confirm focus is now inside the main content region: the announced element should be part of the page’s actual content, not back at the header or navigation.
  5. If the page visibly scrolled but the next Tab press starts from the top again, the target exists but isn’t receiving focus. Check for a missing tabindex="-1".

2.4.1 Bypass Blocks: Pages must provide a mechanism to bypass repeated blocks of content. A skip link that exists but points nowhere satisfies the letter of having “a link” while failing the substance of the criterion, since the bypass mechanism doesn’t actually function.

Page has no skip navigation link is this rule’s companion failure at the opposite end: no skip link exists at all, rather than one existing but pointing at a broken target.

Content in a scrollable region is not keyboard-focusable shares the same underlying mechanism: an element a sighted or mouse user can reach that keyboard focus can’t actually enter.

Keyboard focus becomes trapped in a widget is the inverse direction of a broken focus flow: instead of focus failing to arrive somewhere, it fails to leave.

Visual focus order does not match DOM order covers a related focus-management failure worth checking on any page complex enough to need a skip link in the first place.

Content inside a frame is not keyboard-focusable is the frame-specific version of the same “reachable visually, unreachable by keyboard” problem this rule documents for skip-link targets.

References

Frequently asked questions

Is a broken skip link worse than having no skip link at all?

In one specific way, yes. A missing skip link is a known gap a keyboard user learns to work around by Tabbing through the navigation every time. A broken one looks like it should work, so a user activates it expecting to land on the content, gets nothing, and has to figure out whether the page is broken or they made a mistake before falling back to Tabbing anyway.

Does every skip link need a visible target heading?

No. The target just needs to be a real element the browser can move focus to, most commonly the <main> element with tabindex="-1". It does not need its own visible heading or label, since the skip link text itself already tells the user where they are going.

Why does a <div id="main"> not work as a skip link target even though the id matches?

A matching id is necessary but not sufficient. The browser can scroll a <div> into view when the link is activated, but a <div> has no tabindex by default, so it cannot receive keyboard focus. The user sees the page scroll but their next Tab press starts back at the top, because focus never actually moved.

Does this rule apply to in-page anchor links that are not skip links, like a table of contents?

No. RedFlag's detector only flags anchor links whose text contains a skip-navigation-style word (skip, jump, or main), so a table-of-contents link reading "Section 3" is out of scope for this specific check, even if its target has the exact same missing-or-unfocusable problem.

Should the skip link target use tabindex="-1" or tabindex="0"?

Use tabindex="-1". It lets the element receive focus programmatically, when the skip link is activated, without adding it to the page's regular Tab order. tabindex="0" would insert an extra, confusing stop into the Tab sequence every time a keyboard user reaches that point on the page, which is not the goal here.