Implementing Skip Links Correctly
AI generated
A11Y
WCAG
Accessibility · Keyboard Navigation · Skip Links · Frontend
Implementing Skip Links Correctly
Visible on focus, with a real jump target, keyboard-tested

Without a skip link, keyboard users and screen reader users must tab through the entire navigation on every single page load before reaching the actual content. A correctly implemented skip link, hidden until focused, with a real jump target and a working tabindex, solves this problem reliably and makes every page immediately usable.

12 min read Skip Links · Keyboard Operability · WCAG 2.4.1 Magento 2.4.8 · Hyva Theme · Alpine.js

1. Why skip links are indispensable

A skip link is a link, usually the very first focusable link in the DOM, that jumps directly to the main content of a page and bypasses the entire navigation, the header, and every region that precedes it. For mouse users, the problem it solves is barely visible: clicking a menu item or a product tile takes just as long as any other click. For keyboard users and screen reader users, reality looks different, because they have to move element by element through every navigation item with the Tab key before they even reach the actual page content.

On a typical Magento store with a mega menu, language switcher, cart icon and search field in the header, that often means twenty to forty Tab presses, and on every single page again, because the header repeats globally. This exact repeating pattern is what WCAG success criterion 2.4.1 "Bypass Blocks" at Level A addresses: recurring blocks of content must be bypassable. A missing or incorrectly implemented skip link is therefore not just a convenience issue, it is a documented accessibility violation that shows up first in almost every WCAG audit.

2. How a skip link works technically

Technically, a skip link starts out as an entirely ordinary anchor link with an href that points to a fragment within the same page, for example href="#main-content". On activation, the browser jumps to the element with the matching id and scrolls it into view. The crucial difference from an ordinary jump anchor is that a skip link must not only scroll, it must also move keyboard focus, so the next Tab press actually continues within the main content instead of jumping back to the top of the page.

Modern browsers automatically move focus to the target element during fragment navigation, but only if that element is focusable in the first place. A plain <div> or <main> without a tabindex is not, which means focus silently ends up on <body> and the tab order starts over from the very top. This exact silent failure is the most common reason skip links do not work in practice, even though the link itself is visible and clickable.


<!-- WRONG: skip link points to a target with no focus capability -->
<a href="#main-content" class="skip-link">Skip to main content</a>
...
<main id="main-content">
  <!-- No tabindex: the click scrolls, but focus stays stuck on the link -->
</main>

<!-- RIGHT: skip link as the first focusable element in the DOM,
     target is programmatically focusable via tabindex="-1" -->
<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <header>
    <!-- Navigation, search, cart ... -->
  </header>
  <main id="main-content" tabindex="-1">
    <h1>Product catalog</h1>
    <!-- Main page content -->
  </main>
</body>

3. Visual visibility: hidden until focused

A skip link should be invisible by default for sighted mouse users, because it offers no value to that group and would only clutter the layout. As soon as the link is focused via the Tab key, though, it must become visible, legible, and sufficiently contrasted, otherwise a sighted keyboard user has no idea they just focused an active but invisible link. The correct pattern for this is the so-called sr-only technique: the link is positioned outside the visible area with CSS, but stays in the layout flow and in the DOM, instead of being removed entirely with display: none.

display: none and visibility: hidden are off-limits for skip links, because both also remove the element from the tab order. An element that falls out of the tab order can, by definition, never be focused, which renders the entire skip link useless without that ever showing up in a purely visual review. The correct technique instead moves the element off-screen with position: absolute and a negative offset, and pulls it back into view with a dedicated :focus rule.


/* sr-only: visible only to assistive technology and keyboard focus */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0.5rem;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background-color: #18181b;
  color: #ffffff;
  border-radius: 0.5rem;
  font-weight: 600;
  text-decoration: none;
  /* No display: none and no visibility: hidden here,
     both would remove the element from the tab order */
}

/* Bring the link back into view on keyboard focus */
.skip-link:focus {
  top: 0.5rem;
  outline: 3px solid #f4f4f5;
  outline-offset: 2px;
}

4. The jump target: a real element and tabindex

The jump target of a skip link must satisfy two conditions at once: it needs a unique id that the href can reference, and it must be programmatically focusable. For container elements like <main>, which are naturally not part of the tab order, tabindex="-1" is the correct attribute. A value of -1 means the element becomes focusable via JavaScript or fragment navigation but is not part of the regular tab order, which is exactly the desired behavior: no additional, redundant tab stop, but a valid jump target.

One detail that is frequently overlooked: setting tabindex="0" instead of -1 on the main content element turns it into a regular tab stop, and it shows up a second time on every subsequent tab through the page, which confuses screen reader users because a meaningless focus stop appears in the middle of the content. The value -1 is therefore not a minor detail, it is the decisive difference between a clean jump target and a broken one. It is also worth not removing the focus ring on the target entirely, so sighted keyboard users can see exactly where focus actually landed after the jump.


<!-- WRONG: tabindex="0" turns the main content element into an
     additional, meaningless tab stop on every pass through the page -->
<main id="main-content" tabindex="0">

<!-- RIGHT: tabindex="-1" makes the element programmatically focusable
     without adding it to the regular tab order -->
<main id="main-content" tabindex="-1" class="focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500">
  <h1 class="sr-only">Product catalog: hiking boots</h1>
  <!-- Heading as an extra orientation point for screen readers -->
</main>

For most pages, a single skip link to the main content is entirely sufficient, because it solves the largest and most common problem: tabbing through the global navigation. On more complex page structures, for example a category page with an extensive filter sidebar, a multi-level breadcrumb, and a mini cart widget in the header, a second skip link can make sense, one that jumps directly to the filter sidebar or the product list instead of only to the broad main content region.

When using multiple skip links, a shared, semantically marked-up group right at the top of the page is recommended, for example a <nav aria-label="Skip links"> containing a list of links that become visible one after another as they receive focus. Order matters here: the link to the main content should always come first, with supplementary targets such as "Skip to search" or "Skip to filters" following after it. More than three or four skip links become counterproductive, since they turn into a list that itself has to be tabbed through.

6. Skip links in dynamic Hyva and Alpine.js interfaces

In a classic server-rendered page load, the fragment navigation of a skip link works reliably, because the target element already exists fully in the DOM at initial load. In Hyva themes with Alpine.js components that load content dynamically, for example a live search overlay or an AJAX-updated cart drawer, the structure can change after the first keyboard pass without a full page navigation taking place. The skip link itself is unaffected by this, as long as it keeps pointing to a stable, permanently present element such as #main-content.

Things get more critical with genuine content swaps within a page, for example when a modal or a live search results list opens. In that case, the classic skip link to the main content is not enough, because the new, relevant content no longer lives inside the original #main-content region at all. This is where active focus management via Alpine.js is needed, moving focus deliberately to the first meaningful content of a newly opened view, analogous to the principle of a skip link, only triggered programmatically instead of via a visible link.


// Alpine.js: focus management when opening the live search overlay
// Same principle as a skip link, only triggered programmatically

document.addEventListener('alpine:init', () => {
  Alpine.data('liveSearch', () => ({
    open: false,

    openSearch() {
      this.open = true;
      // Move focus to the new content, analogous to a skip link target
      this.$nextTick(() => {
        const resultsHeading = this.$refs.resultsHeading;
        if (resultsHeading) {
          resultsHeading.setAttribute('tabindex', '-1');
          resultsHeading.focus();
        }
      });
    },

    closeSearch() {
      this.open = false;
      // Return focus to the triggering search field, not lost on body
      this.$refs.searchInput.focus();
    }
  }));
});

7. Styling without breaking the layout with Tailwind

In a Hyva theme, the sr-only technique can be implemented directly with Tailwind utility classes, without writing any custom CSS. The sr-only class hides the element visually, while focus:not-sr-only makes it visible again on focus. A sufficiently high z-index is important, so the visible skip link is not covered by a sticky header or a cookie banner, along with a fixed position relative to the viewport so the link appears in the same place even on a scrolled page.

A second, frequently overlooked point is contrast in the focused state. Because the skip link is invisible by default, it is usually not considered during a visual design review and ends up with arbitrary default colors that, on closer inspection, fall below the WCAG contrast ratio of 4.5:1. The focus indicator itself should also never be removed with outline: none without being replaced by an equally visible alternative, such as a strong border or shadow.

8. Testing: keyboard, screen reader and automation

The most reliable test for a skip link is refreshingly simple and takes only a few seconds: load the page fresh, ignore the mouse entirely, press Tab once. Does the skip link appear visibly at the top of the screen? Press Enter. Does the visible focus ring actually move to the main content, rather than just scrolling the page? A further Tab press afterward should land directly on the first interactive element within the main content, not back on the navigation. This simple manual test reliably catches the most common implementation mistakes.

It is also worth running a screen reader test with NVDA or VoiceOver, since it additionally checks whether the skip link is announced correctly and whether a meaningful announcement occurs at the jump target, for example via a fitting heading right inside the focused region. For automation, an end-to-end test with Playwright that simulates pure keyboard interaction is the right tool. Tools like axe-core detect missing landmark structures, but do not reliably verify whether a skip link actually works after a real keyboard Tab-and-Enter sequence, which is why a dedicated end-to-end test remains essential.


// Playwright: end-to-end test for the actual keyboard behavior
// of a skip link, not just its presence in the DOM

import { test, expect } from '@playwright/test';

test('skip link moves keyboard focus to main content', async ({ page }) => {
  await page.goto('https://shop.example.com/');

  // First Tab press must focus the skip link
  await page.keyboard.press('Tab');
  const skipLink = page.locator('.skip-link');
  await expect(skipLink).toBeFocused();
  await expect(skipLink).toBeVisible();

  // Enter activates the jump
  await page.keyboard.press('Enter');

  // Focus must actually land on the main content, not just scroll to it
  const mainContent = page.locator('#main-content');
  await expect(mainContent).toBeFocused();
});

9. Skip link implementations compared

The following overview summarizes the most common mistakes in skip link implementations and pairs each one with the correct solution. It works well as a quick checklist for code reviews on Hyva templates.

Mistake Impact Correct solution Benefit
Skip link hidden with display: none Not focusable, Tab skips right past it sr-only with focus:not-sr-only Stays reachable in the tab order
Target element with no tabindex Click only scrolls, focus stays on the link tabindex="-1" on the target element Focus actually moves along with the jump
href="#" with no real jump target Jump lands nowhere or at the very top href="#main-content" with a matching id Jump lands exactly on the main content
Skip link placed after logo and search in the DOM User has to tab several times first First focusable element in the DOM Immediate access after the first Tab
Only tested with the mouse Keyboard bug goes unnoticed Test exclusively with Tab and Enter Reliably covers the real usage pattern

One thing stands out in this comparison: almost every mistake is invisible during ordinary mouse use and only surfaces with a deliberate keyboard test. That is exactly why a purely visual design review is not enough to guarantee a working skip link, and exactly why the simple Tab-Enter test belongs in every definition of done for new page layouts.

Mironsoft

Keyboard accessibility, skip links and focus management for Magento and Hyva stores

Skip links that actually work with a keyboard?

We audit your store's keyboard operability, implement skip links with a real jump target and working focus management, and add automated end-to-end tests so the jump mechanism does not silently break again.

Skip link audit

Manual keyboard testing of every page template against WCAG 2.4.1 Bypass Blocks

Focus management

tabindex, sr-only styling and Alpine.js focus handling in Hyva templates

E2E testing

Playwright regression tests for keyboard paths in the CI pipeline

10. Summary

A skip link solves a concrete, recurring problem: keyboard users and screen reader users should not have to tab through the entire navigation again on every page load before reaching the actual content. Correct implementation requires three building blocks at once: visual invisibility until focused, achieved through the sr-only technique rather than display: none, a real jump target with a unique id, and a target element that is programmatically focusable via tabindex="-1" without becoming an additional regular tab stop.

Whether a skip link actually works cannot be judged by looking at the code visually, only by a real keyboard test: load the page, press Tab, press Enter, check where focus actually lands. This simple test belongs in every definition of done for new page layouts and can be automated further with tools like Playwright, so a skip link that works today does not silently break again with future layout changes.

Implementing Skip Links Correctly at a Glance

Problem

Without a skip link, keyboard and screen reader users must tab through the entire navigation again on every page.

Visibility

sr-only with focus:not-sr-only instead of display: none, so the link stays focusable and becomes visible on focus.

Jump target

A real id plus tabindex="-1" on the target element, so focus actually moves along with the jump.

Testing

Press Tab, press Enter, check the focus target. Automate it further as a Playwright end-to-end regression test.

11. FAQ: Implementing Skip Links Correctly

1What is a skip link?
Usually the first focusable link in the DOM, jumping directly to the main content and bypassing the navigation and header.
2Why isn't display: none enough?
Removes the element from the tab order, so it can never be focused. sr-only with a focus rule is the correct technique.
3Why tabindex="-1" on the jump target?
Makes container elements focusable without creating an extra regular tab stop. Without it, focus stays stuck on the link.
4Does every page need only one skip link?
Usually one link to the main content is enough. Complex layouts can justify two or three more, but more than that is counterproductive.
5How do I test a skip link correctly?
Load the page, press Tab, check visibility, press Enter, verify the focus target. Keyboard only, never the mouse.
6Scrolling vs. focusing during the jump?
A click can visually scroll without moving focus. Only tabindex="-1" on the target ensures focus actually moves along.
7How do I implement this in Hyva?
As the first element in the body template with sr-only and focus:not-sr-only, href pointing to the main element's id with tabindex="-1".
8Which WCAG criterion covers skip links?
Success criterion 2.4.1 Bypass Blocks at Level A: recurring content blocks must be bypassable.
9Can the link stay invisible even when focused?
No, in the focused state it must be visible and high contrast, otherwise sighted keyboard users have no idea where they are.
10Sticky header covers the skip link?
A sufficiently high z-index in the focused state plus a fixed position relative to the viewport reliably solves the coverage problem.