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.
Table of Contents
- 1. Why skip links are indispensable
- 2. How a skip link works technically
- 3. Visual visibility: hidden until focused
- 4. The jump target: a real element and tabindex
- 5. Multiple skip links: navigation, search, main content
- 6. Skip links in dynamic Hyva and Alpine.js interfaces
- 7. Styling without breaking the layout with Tailwind
- 8. Testing: keyboard, screen reader and automation
- 9. Skip link implementations compared
- 10. Summary
- 11. FAQ
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>
5. Multiple skip links: navigation, search, main content
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.