/ โ† Accessibility Fixes / Skip to Main Content Link
In this guide
  1. What a skip link is and what WCAG asks for
  2. Why skip to main content is not working
  3. How to test and diagnose it
  4. A skip link that works everywhere
  5. Platform notes
  6. Worked example
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping skip links working
  10. Questions and answers

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 criterionLevelWhat it means for the skip link
2.4.1 Bypass BlocksAThere must be a way to skip repeated header and navigation
2.4.3 Focus OrderAFocus must move to the content, not stay at the top
2.4.7 Focus VisibleAAThe 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 seeLikely cause
Nothing happens when you press EnterThe target id does not exist, is misspelt, or appears twice on the page
The page jumps but the next Tab goes back to the menuFocus did not move, often because a script cancelled the link or the target cannot take focus
The link never appears when tabbingHidden with display: none or visibility: hidden, which makes it unfocusable
The link appears but is not the first stopA cookie banner, announcement bar or chat widget comes earlier in the HTML
It goes to the home pageA <base href> tag makes #main resolve to the site root, or the href includes a full address
Content lands under the headerA fixed or sticky header covers the top of the target
Works on one template onlyOther 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.

๐Ÿ’ก Safari on a Mac does not move to links with Tab by default. Turn on the option to press Tab to highlight each item in Safari's advanced settings, or use Option and Tab, before deciding the link is broken.

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.

โš ๏ธ Never hide the skip link with 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

Worked example

Example Bakery's site had a skip link that appeared on Tab but did nothing. The checks showed:

  1. The theme's link pointed to #content, but the page builder template wrapped the content in a section with no id.
  2. A smooth scroll script attached to every a[href^="#"] called preventDefault() and scrolled the window, without moving focus.
  3. 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

Keeping skip links working

  1. Build the skip link and the id="main" target into the base template, so new page layouts inherit both.
  2. Add a keyboard check to your release routine: Tab, Enter, Tab on the home page and one inner page.
  3. Check the first Tab stop after installing any banner, chat or consent tool.
  4. 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.
Questions and answers

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.

Check your page for keyboard and bypass block problems

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 โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Accessibility Fixes  ·  Keyboard Navigation Fixes  ·  Focus Indicator Fixes  ·  Anchor Jump Links Not Working  ·  Accessibility Fixes in WordPress  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.