Skip Links & Bypass Blocks
A skip link exists mainly for one group of people: sighted keyboard users who never open a screen reader. Screen reader users bypass repeated content with landmarks and headings and rarely touch it. That single fact decides how you build one, where it goes, and why it must appear on focus. This guide covers the working skip link end to end, the number one bug that makes skip links silently fail, and the landmarks and headings that do the real bypass work. Mapped to WCAG 2.2, with copy-ready code.
What Bypass Blocks Actually Solves
Nearly every page on a site repeats the same chrome at the top: a logo, a primary navigation menu, maybe a search box and a row of utility links. A mouse user ignores all of it and clicks straight into the content. A keyboard user cannot. Without a way to bypass those repeated blocks, they must press Tab through every one of those links, on every page they visit, before they reach anything new. WCAG 2.4.1 Bypass Blocks (Level A) requires that a mechanism exists to skip that repetition.
The criterion can be satisfied in three ways, and the most important thing to understand before writing any code is that the three mechanisms serve different people:
- A skip link is a link near the top of the page that jumps focus to the main content. It is the only fast bypass available to a sighted keyboard user who does not run a screen reader, a group that includes people with motor disabilities, people using switch access or a keyboard for speed, and anyone whose mouse is unavailable.
- Landmark regions let a screen reader user jump straight to
main,navigation, or any other region with a single command. This is how most screen reader users actually bypass repeated content. - A logical heading structure lets a screen reader user move heading to heading, skimming the page the way a sighted reader skims with their eyes.
The reframe that decides everything
The skip link is for keyboard users who can see; landmarks and headings are for screen reader users. Because its audience can see, a skip link that stays hidden helps nobody, which is exactly why it must become visible on focus. And because you have two separate audiences, you owe all three mechanisms, not one instead of the others. Provide a visible-on-focus skip link and correct landmarks and a sound heading outline.
The rest of this guide builds the skip link first, because it is where the subtle failures live, then covers the landmarks and headings that carry the bypass for screen reader users.
How Bypass Blocks Maps to WCAG 2.2
The highlighted row, 2.4.1 Bypass Blocks, is the criterion this guide serves. The rest are the criteria a working skip link, and the landmarks and headings behind it, must also satisfy, from moving focus in a logical order to keeping the target visible under a sticky header.
| Criterion | Level | How it applies to bypassing blocks |
|---|---|---|
| 2.4.1 Bypass Blocks | A | A mechanism must let users skip blocks of content repeated across pages. A skip link, landmark regions, or headings each qualify. |
| 1.3.1 Info and Relationships | A | When landmarks or headings are the bypass, the region and heading structure must exist in the markup, not just look like structure visually. |
| 2.4.3 Focus Order | A | Activating the skip link must actually move focus to the target, in an order that preserves meaning. Focus that stays at the top is the classic failure. |
| 2.4.7 Focus Visible | AA | When the skip link receives focus it must show a clearly visible focus indicator, which is what reveals a link hidden until focus. |
| 2.4.11 Focus Not Obscured | AA | The skip link when shown, and the target once focused, must not be hidden behind a sticky or fixed header. New in WCAG 2.2. |
| 4.1.2 Name, Role, Value | A | Repeated landmarks of the same type, such as two nav regions, need distinct accessible names so a screen reader user can tell them apart. |
| 2.1.1 Keyboard | A | The skip mechanism must be operable by keyboard. In practice this means the target has to be able to receive keyboard focus. |
| 2.4.10 Section Headings | AAA | Organizing content under section headings is an additional way to bypass blocks, useful to keep in mind even though it sits at AAA. |
For the full wording and an interactive demo of the core criterion, see the WCAG 2.4.1 Bypass Blocks reference. The rest of the WCAG 2.2 criteria are one click away.
1. The Minimum Viable Skip Link
A working skip link is three small pieces of markup: a link that is the first focusable element in the document, a target at the start of the main content, and a CSS rule that reveals the link on focus. Here is the whole thing:
<body>
<!-- The very first focusable element on the page -->
<a class="skip-link" href="#main-content">
Skip to main content
</a>
<header>
<a href="/">Acme</a>
<nav aria-label="Primary">
<!-- many links -->
</nav>
</header>
<!-- The target: focusable because of tabindex="-1" -->
<main id="main-content" tabindex="-1">
<h1>Page title</h1>
...
</main>
</body>Two decisions in that markup carry the whole feature, and each is worth naming:
- The link is first in the DOM. It sits before the
<header>, so the first time a keyboard user presses Tab on the page, this is what they land on. Put anything focusable ahead of it, a logo link, a cookie banner, and you have made them tab past that before they can skip, which defeats the point. - The target carries
tabindex="-1". This is the piece teams leave out, and leaving it out is why so many skip links half-work. The next section is entirely about why it is load-bearing.
The link text should say where it goes. Skip to main content is clear and conventional. Avoid vague text like Skip on its own, which does not say skip to where.
2. The CSS: Hidden Until Focused, Never display:none
The standard visual treatment keeps the skip link off-screen until it is focused, then slides it into view at the top of the page. The critical rule is that the hiding technique must keep the link in the tab order. display: none and visibility: hidden both remove an element from the tab order, so a link hidden that way can never be reached and the skip mechanism silently does nothing. Position it off-screen instead:
.skip-link {
position: absolute;
left: 0;
top: 0;
/* Move it off-screen without removing it from the tab order */
transform: translateY(-120%);
padding: 0.75rem 1rem;
background: #1d4ed8;
color: #fff;
border-radius: 0 0 0.375rem 0;
z-index: 1000;
transition: transform 0.15s ease-in-out;
}
/* Bring it into view when a keyboard user tabs onto it */
.skip-link:focus {
transform: translateY(0);
/* A visible focus indicator satisfies 2.4.7 */
outline: 3px solid #fff;
outline-offset: 2px;
}A transform keeps the element rendered and focusable while pushing it out of sight, so it stays in the tab order and animates back on focus. The older clip or off-screen left: -9999px techniques work too, as long as you restore the link to a visible on-screen position on :focus. Whichever you choose, the :focus state must place a clearly visible link in a predictable spot, usually the top-left corner, with an obvious focus indicator.
Do not reuse a generic .sr-only or .visually-hidden utility for a skip link unless it has a paired :focus rule that brings the element back on screen. Many such utilities are designed to keep content permanently invisible, which is the opposite of what a skip link needs: a screen-reader-only skip link that never appears fails the sighted keyboard user it is meant to serve.
3. The Number One Bug: the Target Must Be Focusable
This is the failure that hides in more skip links than any other, and it is not visible on screen. When you click a link to a fragment like #main-content, the browser scrolls the matching element into view, but it moves keyboard focus to that element only if the element can receive focus. Links, buttons, and form fields can. A plain <main>, <div>, or <section> cannot. So the page scrolls, it looks like the skip worked, and then the user presses Tab and focus jumps back to the top of the page, straight into the navigation they were trying to skip.
The one-line fix, and how to prove it worked
Add tabindex="-1" to the target: <main id="main-content" tabindex="-1">. A value of -1 lets an element receive focus programmatically or from a fragment link without adding it to the normal Tab sequence. To prove it works, activate the skip link, then press Tab once more: focus should move to the first interactive element inside the main content, not back to the top. If it returns to the top, the target is not focusable.
tabindex="-1" on a container is safe: it does not make the element a Tab stop, it only makes it a valid focus target. You may see a focus outline flash briefly on the <main>when the skip fires; that is expected and acceptable, and you can style the container’s :focus to suit. Do not, however, set outline: none on it with nothing in its place, or you remove the only cue that focus has moved.
Sticky headers: keep the focused target visible
If your header is position: sticky or fixed, the target can end up scrolled to the very top of the viewport and then covered by that header, so focus has moved but the user cannot see where. That is a 2.4.11 Focus Not Obscured failure. Reserve space with scroll-margin-top on the target so it stops below the fixed header:
#main-content {
/* Match this to the height of your sticky header */
scroll-margin-top: 5rem;
}A related, more robust option is to point the skip link at the page’s <h1> rather than the <main> wrapper, giving the heading the tabindex="-1" and the scroll-margin-top. Landing on the heading means the screen reader announces the page title as focus arrives, which is a helpful confirmation of where the user now is.
4. Landmarks: the Real Bypass for Screen Reader Users
A screen reader user rarely uses the skip link, because they have something faster: landmark navigation. Screen readers let the user pull up a list of every landmark region on the page and jump straight to one, so reaching the main content is a single command regardless of how much navigation sits above it. The HTML5 sectioning elements map to landmark roles automatically, which means you often get this for free by using the right elements:
| Element | Landmark role | Notes |
|---|---|---|
| <header> | banner | Only when it is the page-level header, not inside an article or section. |
| <nav> | navigation | Give each nav a distinct name when a page has more than one. |
| <main> | main | Exactly one per page. This is the skip link’s usual target. |
| <aside> | complementary | Sidebars and related content tangential to the main content. |
| <footer> | contentinfo | Only when it is the page-level footer. |
Three rules keep landmarks useful rather than noisy:
- One
<main>per page. The main landmark is the destination the whole bypass exists to reach. More than one, or none, makes it ambiguous. - Name repeated landmarks. Two
<nav>regions both announce as “navigation” unless you distinguish them witharia-label, for examplearia-label="Primary"andaria-label="Footer". This is the 4.1.2 tie-in. - Do not put the word “navigation” in a nav’s label. The role already says it, so
aria-label="Primary navigation"is announced as “Primary navigation, navigation”. Name itPrimary.
Landmarks are part of the broader semantic structure covered by using ARIA the right way; the first rule there applies here too, prefer the native<nav>, <main>, and <aside> elements over role="navigation" and friends on a <div>.
5. Headings as a Bypass Mechanism
The third bypass mechanism is the one most people do not think of as one: a logical heading structure. Screen reader users navigate by heading constantly, jumping from one to the next to survey a page and move quickly to the part they want. A page with a single <h1> naming the page, and nested <h2> and <h3>headings that follow the content’s outline, lets them skip straight past repeated chrome to the first real heading of the content.
Two habits make headings work as a bypass:
- One
<h1>that names the page. It should describe this page specifically, and it is a natural, robust target for the skip link, since landing there announces the page. - Do not skip levels. Go
<h1>to<h2>to<h3>in order. Jumping from<h1>to<h4>breaks the outline a screen reader user relies on to understand structure.
Organizing content under headings is formally recognized as a bypass technique in 2.4.10 Section Headings. That criterion sits at Level AAA, so it is not required for AA conformance, but the heading navigation it describes is one of the most-used features in every screen reader, so a sound heading outline is worth building regardless of the level.
6. More Than One Skip Link, and Skipping Within Content
One skip link to the main content covers most pages. Some pages have several distinct repeated blocks worth bypassing separately, and there it is reasonable to offer more than one:
<a class="skip-link" href="#main-content">Skip to main content</a>
<a class="skip-link" href="#primary-nav">Skip to navigation</a>
<a class="skip-link" href="#site-search">Skip to search</a>Keep the skip-to-main-content link first, because it is what most users want, and make sure each target carries tabindex="-1" like any other skip target. Resist the urge to add a link for every region; a stack of six skip links becomes its own block of repeated content to tab past.
The same idea works insidecontent, not just at the top of the page. If a page contains a very long data table, a large interactive widget, or an extended list of links that a keyboard user would otherwise have to tab all the way through, a short “skip over this table” link placed just before it, pointing to a focusable element just after it, is a legitimate bypass. It is the same pattern, applied to a repeated or oversized block in the body rather than the site chrome.
7. Skip Links in Single-Page Apps and React
The markup is the same in a single-page app, but client-side routing adds a problem the static case does not have. When the user navigates, the page does not reload, so keyboard focus stays wherever it was, often on a link inside the navigation, and the new view renders around that stale focus. The skip link still helps within a view, but you now also owe deliberate focus management on every route change.
In React, that is a small SkipLink component plus an effect that moves focus to the main region whenever the route changes:
function SkipLink() {
return (
<a className="skip-link" href="#main-content">
Skip to main content
</a>
)
}
// On each route change, move focus to the new view's main region
function useFocusMainOnRouteChange(pathname, mainRef) {
useEffect(() => {
mainRef.current?.focus()
}, [pathname, mainRef])
}
function Layout({ children }) {
const pathname = usePathname()
const mainRef = useRef(null)
useFocusMainOnRouteChange(pathname, mainRef)
return (
<>
<SkipLink />
<SiteHeader />
<main id="main-content" tabIndex={-1} ref={mainRef}>
{children}
</main>
</>
)
}The <main> keeps its tabIndex of -1 so both the skip link and the route-change effect can focus it. Moving focus to the main region on navigation also gives screen reader users a clear signal that the view changed, which client-side routing otherwise swallows. This is one facet of a broader topic; the focus management guide covers route-change focus in depth, and the React accessibility guide puts it in the context of a component architecture.
8. Testing Bypass Blocks
Because the most common skip-link failure is invisible on screen, the keyboard test is the one that matters most, and no automated tool can replace it.
The keyboard test
- Load the page and press Tab once. A visible skip link should appear, with a clear focus indicator.
- Press Enter to activate it, then press Tab again. Focus should land on the first interactive element inside the main content. If it returns to the top of the page, the target is missing
tabindex="-1". - Confirm the revealed link and the focused target are not hidden behind a sticky header (2.4.11).
The screen reader test
Open the landmark list and the heading list, the two bypass mechanisms screen reader users actually use. In NVDA or JAWS, open the elements list; in VoiceOver, use the rotor. Confirm there is exactly one main landmark, that repeated regions have distinct names, and that the heading outline is logical with a single <h1>.
Automated checks
Scanners such as axe and WAVE can flag a missing <main> landmark, a page with no headings, or a heading level that skips, and those are worth catching. What they cannot check is whether the skip link actually moves focus, which is the failure that matters most, so treat a clean automated report as necessary but not sufficient. The automated versus manual testing guide explains where that line falls, and the website audit guide puts these steps in order.
Common Skip Link Mistakes & How to Fix Them
These are the bypass-block errors that turn up most in real-world audits. Every one is a small decision with an outsized effect on whether keyboard users can actually get past your navigation.
| Anti-pattern | Why it fails | The fix |
|---|---|---|
| Hiding the skip link with display:none or visibility:hidden. | Both properties remove the link from the tab order, so a keyboard user can never reach it and the bypass mechanism does nothing at all (fails 2.4.1). | Position the link off-screen with a visually-hidden technique that keeps it focusable, then bring it into view on :focus. Never use display:none or visibility:hidden. |
| A skip link that never becomes visible when focused. | The sighted keyboard user it exists to serve tabs onto an invisible link, sees focus apparently vanish, and has no idea the shortcut is there (fails 2.4.7 and undermines 2.4.1). | Move the link back on-screen on :focus with a clearly visible focus indicator, so pressing Tab once reveals it at the top of the page. |
| The target has no tabindex, so activating the link scrolls but does not move focus. | A <main> or <div> cannot receive focus by default, so the browser scrolls to it but leaves keyboard focus at the top; the next Tab returns the user into the navigation (fails 2.4.3 and 2.1.1 in effect). | Add tabindex="-1" to the target element so it can receive programmatic focus, and confirm focus actually lands there after activation. |
| The skip link is not the first focusable element on the page. | If a cookie banner, logo link, or search field comes first in the DOM, the user must tab through them before reaching the skip link, which defeats much of its purpose. | Place the skip link as the very first focusable element in the document, ahead of the header and any other interactive chrome. |
| Relying on landmarks alone and shipping no skip link. | Landmarks help screen reader users, but a sighted keyboard user who does not run a screen reader has no way to use them and is left tabbing through the whole header on every page. | Provide a visible-on-focus skip link in addition to correct landmarks and headings. The three mechanisms serve different audiences. |
| The skip target lands under a sticky header that hides it. | Focus reaches the target, but a fixed or sticky header covers it, so a sighted keyboard user cannot see where focus went (fails 2.4.11 Focus Not Obscured). | Add scroll-margin-top to the target equal to the header height so it scrolls clear of the fixed header when focused. |
The Bypass Blocks Checklist
- Skip link is first. A skip-to-main-content link is the very first focusable element in the DOM, ahead of the header and any other control.
- It appears on focus. The link is hidden off-screen but comes into view with a visible focus indicator when tabbed to, never using
display:noneorvisibility:hidden. - The target is focusable. The target carries
tabindex="-1"so activating the link moves focus, not just scroll position. - Focus is verified. Activating the link, then pressing Tab, moves focus into the main content and not back to the top of the page.
- The target stays visible. A sticky or fixed header does not cover the focused target;
scroll-margin-topclears it (2.4.11). - One main landmark. The page has exactly one
<main>, plus<header>,<nav>, and<footer>as appropriate. - Repeated landmarks are named. Multiple
<nav>regions have distinctaria-labels that do not include the word “navigation”. - Headings form an outline. A single
<h1>names the page and heading levels descend without skipping. - SPA route changes move focus. In a single-page app, each navigation moves focus to the new view’s main region or heading.
- Tested by keyboard and screen reader. The Tab test confirms focus moves; the landmark and heading lists confirm the screen reader bypass.
Get Past the Navigation
Start from the criterion this serves, then wire the focus behaviour that makes a skip link, and every route change, land where it should.
Frequently Asked Questions
What is a skip link and who is it actually for?▾
A skip link is a link, usually the first focusable element on the page, that jumps a keyboard user past repeated blocks such as the header and primary navigation straight to the main content. Its primary audience is narrower than most people assume: sighted keyboard users who do not run a screen reader. A screen reader user already has faster ways to bypass repeated content, jumping by landmark region or by heading, and rarely reaches for the skip link. A sighted keyboard user has none of those shortcuts, so without a skip link they must press Tab through every navigation link on every page just to reach the content. That is why the skip link must become visible when it receives focus: if it stays hidden, the one group it exists to serve cannot see that it is there.
Why does my skip link scroll the page but not move keyboard focus?▾
This is the single most common skip-link bug, and it is not a CSS problem. Activating a link to a fragment such as #main-content moves keyboard focus to the target only if that target is an element that can receive focus. Links, buttons, and form fields can; a plain <main>, <div>, or <section> cannot by default. So the browser scrolls the target into view but leaves focus back at the top of the page, and the user's next Tab press sends them right back into the navigation they were trying to skip. The fix is to make the target focusable by adding tabindex="-1" to it, for example <main id="main-content" tabindex="-1">. A value of -1 lets the element receive programmatic and click-driven focus without adding it to the normal Tab order. With that in place, activating the skip link both scrolls to and focuses the main content, and the next Tab continues from there.
Does a skip link have to be visible?▾
It does not have to be visible all the time, but it must become visible when it receives keyboard focus. The standard pattern positions the link off-screen until it is focused, then brings it into view at the top of the page. What you must not do is hide it with display:none or visibility:hidden, because both remove the link from the tab order entirely, so a keyboard user can never reach it and the skip mechanism does nothing. A skip link that is always visible is perfectly valid too, and some design systems keep it on screen deliberately; hiding it until focus is a visual-design choice, not an accessibility requirement. The accessibility requirement is only that a keyboard user can reach it and see it when they do.
Isn't a skip link redundant if I already have proper landmarks and headings?▾
No, because landmarks, headings, and skip links serve overlapping but different audiences. Landmark regions and a logical heading structure let screen reader users jump over repeated content, and they are the primary bypass mechanism for those users. But a sighted keyboard user who does not run a screen reader gets no benefit from landmarks or headings; their only fast way past the navigation is a skip link. Support also varies: landmark navigation is well supported in modern screen readers but not universal, so a skip link is the most reliable single technique. The right approach is to provide all three: a visible-on-focus skip link for keyboard users, correct landmark regions, and a sound heading outline. They reinforce each other rather than duplicate work.
Where should the skip link go, and where should it point?▾
The skip link should be the very first focusable element in the DOM, so it is the first thing a keyboard user reaches when they press Tab on a fresh page. If a cookie banner, a logo link, or a search box comes before it in source order, the user has to tab through those first and the skip link loses much of its value. The target should be the start of the main content, normally the page's single <main> element or the primary content container. Give that target tabindex="-1" so it can receive focus, point the link's href at its id, and confirm that activating the link both moves focus to the target and scrolls it into view. Pointing a skip link at a target that cannot receive focus is the usual reason a skip link appears to do nothing.
How many skip links should a page have?▾
One well-placed skip link to the main content covers most pages. Add more only when a page has several distinct blocks of repeated content worth bypassing, for example a site with a mega-menu, a secondary toolbar, and a search region, where separate "Skip to navigation", "Skip to search", and "Skip to main content" links each save real effort. The first link should still be the skip-to-main-content one, since that is what most users want. You can also use the same idea inside content: a link that lets keyboard users jump over a very long data table, a large embedded widget, or an extended list of links is a legitimate bypass mechanism. Do not overdo it; a wall of skip links at the top of every page becomes its own block to tab through.
How do skip links work in a single-page app or React?▾
The mechanics are the same, but single-page apps add a focus-management problem. When the user navigates client-side, the page does not reload, so browser focus stays wherever it was and the skip link's target may still point at the previous view. Two things fix this. First, keep a skip link as the first focusable element and point it at the main region as usual, so it works within any given view. Second, on each route change move focus deliberately to the new view's main region or its heading, so the user starts from the content rather than from stale focus, and so the skip link's target is the current one. In React that is a SkipLink component plus an effect that runs on navigation and calls focus() on the main element (which carries tabindex="-1"). This overlaps with route-change focus management generally, covered in the focus management guide.
How do I test that bypass blocks work?▾
Start with the keyboard, because that is the audience the skip link serves. Load the page, press Tab once, and confirm a visible skip link appears with a clear focus indicator. Activate it with Enter, then press Tab again and confirm the next focus stop is inside the main content, not back in the navigation; if it returns to the top, the target is missing tabindex="-1". Then test the screen reader bypass: open the screen reader's list of landmarks and confirm there is one main region and that repeated regions such as multiple navs have distinct names, and open its list of headings and confirm the outline is logical with a single h1. Automated tools such as axe and WAVE can flag a missing main landmark or a page with no headings, but they cannot tell you whether the skip link actually moves focus, so the keyboard pass is essential.
Essential Accessibility Resources
Comprehensive tools, checklists, and guides to help you create inclusive digital experiences