Skip to Main Content Link Not Working: How to Fix WCAG Skip Links
A skip to main content link lets keyboard users jump past the header and menu to the page content with a single key press. When it does nothing, never appears, or drops focus back at the top of the page, people who cannot use a mouse have to tab through every menu item on every page, and the site fails WCAG 2.4.1 Bypass Blocks. The usual causes are small: a missing or duplicated target id, a target that does not take focus, a link hidden in a way that removes it from the Tab order, a script that cancels hash links, or a sticky header covering the content. This guide shows how to test it, find the cause and build a skip link that works on WordPress, Shopify and custom sites.
What a skip link is and what WCAG asks for
A skip to main content link is the first focusable item on a page. It is usually hidden until someone presses Tab, then appears at the top of the screen. Activating it moves keyboard focus past the header, logo, search box and menu, straight to the start of the page content.
Without it, a keyboard user has to tab through every menu link on every page before reaching the content. On a site with a mega menu that can be fifty or more key presses per page. Screen reader users have other ways to jump around, but sighted keyboard users, switch users and people using voice control often rely on the skip link.
WCAG success criterion 2.4.1, Bypass Blocks, at Level A, asks for a way to skip blocks of content that repeat across pages. WCAG accepts more than one technique: a skip link, ARIA landmarks such as <main> and <nav>, or a sensible heading structure. Browsers give sighted keyboard users no built-in shortcut to jump between landmarks, though, so a working skip link plus a proper <main> element is the dependable way to meet it.
| WCAG criterion | Level | What it means for the skip link |
|---|---|---|
| 2.4.1 Bypass Blocks | A | There must be a way to skip repeated header and navigation |
| 2.4.3 Focus Order | A | Focus must move to the content, not stay at the top |
| 2.4.7 Focus Visible | AA | The link must be visible when it has focus |
| 2.4.11 Focus Not Obscured (Minimum) | AA (WCAG 2.2) | A sticky header must not completely hide the focused content |
Why skip to main content is not working
| What you see | Likely cause |
|---|---|
| Nothing happens when you press Enter | The target id does not exist, is misspelt, or appears twice on the page |
| The page jumps but the next Tab goes back to the menu | Focus did not move, often because a script cancelled the link or the target cannot take focus |
| The link never appears when tabbing | Hidden with display: none or visibility: hidden, which makes it unfocusable |
| The link appears but is not the first stop | A cookie banner, announcement bar or chat widget comes earlier in the HTML |
| It goes to the home page | A <base href> tag makes #main resolve to the site root, or the href includes a full address |
| Content lands under the header | A fixed or sticky header covers the top of the target |
| Works on one template only | Other templates lack id="main" on their main element |
A missing or wrong target is by far the most common cause. Page builders often output the content wrapper without the id the theme's skip link points to, and duplicate ids from copied sections can send the link to the wrong place; see duplicate id fixes.
How to test and diagnose it
Test it the way a keyboard user would. Load the page, click in the address bar, press Tab once. The skip link should appear. Press Enter, then press Tab again. Focus should land on the first link or button inside the content, not on the menu.
Check the HTML the server sends:
curl -s https://example.com/ | grep -oiE 'href=.#[A-Za-z0-9_-]+.' | head -5
curl -s https://example.com/ | grep -oE 'id="(main|content|maincontent|primary)"' | sort | uniq -c
curl -s https://example.com/ | grep -i '<base '
The first line shows the skip link and its target. The second should print the matching id exactly once. The third should print nothing; if it finds a <base> tag, fragment links resolve against that address instead of the current page.
Then check where focus goes, in the DevTools console after pressing the skip link:
document.querySelector('#main') // should not be null
document.querySelectorAll('[id="main"]').length // should be 1
document.activeElement // should be the main element
If document.activeElement is still the skip link or the body, focus did not move. Look in the Event Listeners pane for click handlers on a[href^="#"]; smooth scrolling scripts and single-page app routers often call preventDefault() on every hash link.
A skip link that works everywhere
Put the link immediately after the opening <body> tag, before any banner or header, and give the main element a matching id and tabindex="-1":
<body>
<a href='#main' id="skip-link">Skip to main content</a>
<header>...logo, search, navigation...</header>
<main id="main" tabindex="-1">
<h1>Opening hours</h1>
...
</main>
</body>
Current browsers move the focus starting point to the target of a fragment link even when the target is not focusable, but tabindex="-1" makes the move dependable across browsers and screen readers without adding the main element to the Tab order.
Hidden until focused, not hidden for good
#skip-link {
position: absolute;
left: 1rem;
top: -10rem;
z-index: 10000;
padding: 0.75rem 1rem;
background: #ffffff;
border: 2px solid #1a1a1a;
font-weight: 700;
}
#skip-link:focus {
top: 1rem;
outline: 3px solid #1a1a1a;
outline-offset: 2px;
}
main { scroll-margin-top: 6rem; }
The link sits off screen until it receives focus, then slides into view with a clear outline, which meets Focus Visible. The scroll-margin-top value should match the height of your sticky header so the start of the content is not hidden beneath it. Keep the default link text shade or another with strong contrast; see colour contrast fixes.
display: none or visibility: hidden, even on desktop. Elements hidden that way cannot receive focus, so the link is gone for exactly the people who need it.If a script blocks hash links
document.getElementById('skip-link').addEventListener('click', (e) => {
const target = document.getElementById('main');
if (!target) return;
e.preventDefault();
target.focus();
target.scrollIntoView();
});
Use this only when a router or smooth scroll script stops the normal link from working and you cannot exclude the skip link from that script.
Platform notes
- WordPress block themes. WordPress adds a skip link automatically to block themes and points it at the
<main>element, adding an id if needed. If your template has no group set to the main HTML element, there is nothing to skip to. Open the template in the site editor, select the content group, and set its HTML element to main under Advanced. - WordPress classic themes and page builders. The theme usually prints a link to
#contentor#primary. Page builder templates that replace the theme's content area can drop that id. Add it back through the builder's CSS ID field on the main section. More in accessibility fixes in WordPress. - Shopify. Most current themes include a skip link to
#MainContent. Custom sections or apps that add a banner before it can push it down the focus order, so check the first Tab stop after installing apps. - React, Next.js and other single-page apps. Use a plain anchor element for the skip link, not a router link component, and make sure the main element with its id is present on every route. After client-side navigation, move focus to the main heading or main element so keyboard users start at the new content.
Worked example
Example Bakery's site had a skip link that appeared on Tab but did nothing. The checks showed:
- The theme's link pointed to
#content, but the page builder template wrapped the content in a section with no id. - A smooth scroll script attached to every
a[href^="#"]calledpreventDefault()and scrolled the window, without moving focus. - The sticky header was 88 pixels tall and covered the first heading after the jump.
The fix added id="content" and tabindex="-1" to the main section through the builder's settings, excluded #skip-link from the smooth scroll script with :not(#skip-link) in its selector, and set scroll-margin-top: 6rem on the target. After the change, pressing Enter on the skip link and then Tab moved focus to the first link in the content, with the heading in full view. It took two keystrokes to reach the content instead of 34.
The fix, step by step
Step 1
Test with the keyboard
Open the page, click in the address bar and press Tab once. The skip link should appear at the top of the page. Press Enter, then press Tab again and note where focus lands. If it lands on the first link in the content, the link works. If nothing appears, or focus goes back to the menu, write down which of these happened, because it points to the cause.
Step 2
Check the target id exists once
Read the skip link's href, such as #main, and confirm an element with exactly that id exists on the page with document.querySelectorAll('[id="main"]').length in the console, which should return 1. If it returns 0, add the id to the main content wrapper in the template or page builder. If it returns more than 1, rename the duplicates. Retest on every template, not just the home page.
Step 3
Make the target take focus
Add tabindex="-1" to the target element so browsers and screen readers reliably move focus to it, and use a <main> element where possible so it is also a landmark. Press the skip link again, then check document.activeElement in the console. It should now return the main element, and the next Tab should move to the first link or button inside the content rather than back to the menu.
Step 4
Show the link on focus
Replace any display: none or visibility: hidden on the skip link with off-screen positioning that moves the link into view on :focus, with a clear outline and solid background. Do not hide it on any screen size. Press Tab on desktop and on a narrow window to confirm it appears straight away, is readable against the page and is not covered by the header or a banner.
Step 5
Make it the first Tab stop
Move the skip link so it is the first element after the opening <body> tag, ahead of cookie banners, announcement bars and chat buttons. In WordPress or Shopify this usually means checking the theme header file and any app that injects markup at the top of the body. Reload the page and confirm the very first Tab press lands on the skip link every time.
Step 6
Stop scripts blocking the link
Check DevTools Event Listeners for click handlers on hash links. If a smooth scroll script or router calls preventDefault(), exclude the skip link from its selector, or add a small handler that focuses the target and scrolls it into view. Check for a <base href> tag with curl and grep, and remove it or use the full page path. Retest Enter then Tab afterwards on each template.
Step 7
Clear the sticky header
If the content lands under a fixed header, set scroll-margin-top on the target to the header height plus a little space, for example 6rem. This keeps the first heading visible after the jump and meets Focus Not Obscured in WCAG 2.2. Test at several widths, because headers often grow taller on small screens when the menu wraps onto two lines or a promotional banner appears.
Step 8
Confirm with a screen reader and audit
Test with NVDA on Windows or VoiceOver on a Mac. Activate the skip link and listen: the reader should move to the main content and read the heading. Then run an accessibility audit across key templates to check bypass blocks, landmarks and focus visibility. Record the result and add the keyboard test to the routine checks you run after every theme, plugin or template change.
Edge cases worth knowing
- More than one skip link. Extra links such as skip to search or skip to footer are allowed, but keep the first one as skip to main content and keep the list short.
- Cookie and consent banners. A banner placed before the skip link in the HTML takes the first Tab stop. Move the skip link above it in the source, or make sure the banner can be dismissed quickly with the keyboard.
- Visible text. Write the link as plain words, such as Skip to main content. An icon or a link that only reads Skip is unclear to voice control users.
- Anchor links elsewhere. If in-page jump links on the same site also fail, the cause is usually shared; see anchor jump links not working.
- Focus outline on main. With
tabindex="-1", some browsers draw an outline around the whole content area after the jump. That is acceptable. If you hide it, hide it only for the main element and keep strong focus styles everywhere else.
Keeping skip links working
- Build the skip link and the
id="main"target into the base template, so new page layouts inherit both. - Add a keyboard check to your release routine: Tab, Enter, Tab on the home page and one inner page.
- Check the first Tab stop after installing any banner, chat or consent tool.
- Run a full keyboard review now and then; the keyboard navigation guide and focus indicator fixes cover the wider checks, and checking WCAG compliance puts them in context.
1. Why is my skip to main content link not working?
A skip to main content link usually fails because its target id does not exist, is misspelt or appears twice on the page. Other common causes are a script that cancels hash links, a target the browser does not move focus to, a base tag that sends the link to the home page, and a sticky header that hides the content after the jump. Check the href, confirm one element has that id, add tabindex of minus one to it, and test again with Enter then Tab.
2. Is a skip to content link required by WCAG?
A skip link is not strictly required by WCAG, but WCAG 2.4.1 Bypass Blocks at Level A requires some way to skip content that repeats on every page. WCAG accepts skip links, ARIA landmarks or proper headings as ways to meet it. In practice, a skip link plus a main landmark is the dependable choice, because sighted keyboard users have no built-in way to jump between landmarks. Most accessibility audits and public sector checks expect to find one.
3. Where should the skip link point?
The skip link should point to the start of the unique content on the page, usually the main element. Give that element an id such as main, set the link href to the same id with a hash in front, and add tabindex of minus one so focus moves reliably. Do not point it at the first heading inside a hero image or at a wrapper that also contains the navigation, or the link will not skip anything useful.
4. Should the skip link be hidden?
The skip link can be hidden until it receives keyboard focus, but it must become visible when focused. Use off-screen positioning that brings it into view on focus, rather than display none or visibility hidden, which stop it receiving focus at all. A visible link meets WCAG 2.4.7 Focus Visible and helps sighted keyboard users who do not know it exists. Some sites show it permanently, which is also fine and avoids the question entirely.
5. Why does the skip link work in Chrome but not Safari?
The skip link often seems broken in Safari because Safari on a Mac does not move between links with the Tab key by default. Turn on the setting to press Tab to highlight each item in Safari's advanced preferences, or use Option and Tab. Once that is on, a correctly built skip link with a matching id and tabindex of minus one on the target should work the same way as in Chrome, Edge and Firefox.
6. Does a skip link need tabindex on the main element?
Adding tabindex of minus one to the main element is not always needed in current browsers, but it is recommended because it makes focus move reliably. Modern browsers move the starting point for the next Tab press to a fragment target, yet some combinations of browser, screen reader and script still leave focus behind. Minus one lets the element receive focus from the link without adding it to the normal Tab order, so it has no downside for other users.
7. Does WordPress add a skip link automatically?
WordPress adds a skip link automatically to block themes, pointing it at the main element of the template. If the template has no group set to the main HTML element, the link has nowhere to go, so set the content group to main in the site editor. Classic themes print their own skip link, usually to an id such as content, which page builders can remove when they replace the content area. Check the first Tab stop after any theme change.
The accessibility audit on aiwebpageseo.com checks your page for landmarks, skip links, focus problems and other WCAG issues, so you can see which templates still fail Bypass Blocks.
Open Accessibility Fixes โ