Skip Link Generator
A skip link that is hidden until focused, with the CSS that keeps it available to screen readers and the tabindex the target needs to actually receive focus.
<!--
Skip link. First thing inside <body>, before the header, so it is the first
thing a keyboard user reaches.
WCAG 2.4.1 Bypass Blocks. Without one, getting past a navigation of thirty
links means thirty tab presses on every page.
-->
<a class="skip-link" href="#main">Skip to main content</a>
<!--
tabindex="-1" makes the target programmatically focusable. Without it, several
browsers scroll to the element and leave keyboard focus back on the link, so
the next Tab press goes to the second item in the navigation.
-->
<main id="main" tabindex="-1">
<!-- Page content. -->
</main>
<style>
/*
Hidden but available: clipped to nothing rather than display:none or
visibility:hidden, because both of those take the link out of the
accessibility tree and a screen reader user never finds it.
*/
.skip-link {
position: absolute;
z-index: 999;
top: 8px;
left: 8px;
padding: 0.75rem 1rem;
background: #1e56ec;
color: #ffffff;
border-radius: 4px;
font: inherit;
text-decoration: underline;
transform: translateY(calc(-100% - 8px - 0.5rem));
transition: transform 150ms ease-in-out;
}
.skip-link:focus,
.skip-link:focus-visible {
transform: translateY(0);
}
/* The target keeps no focus ring of its own, but the link must. */
.skip-link:focus-visible {
outline: 3px solid currentColor;
outline-offset: 2px;
}
#main:focus {
outline: none;
}
@media (prefers-reduced-motion: reduce) {
.skip-link {
transition: none;
}
}
</style>
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
A skip link lets a keyboard user get past the navigation. Without one, reaching the content on a site with thirty header links means thirty tab presses, on every page. That is WCAG 2.4.1, Bypass Blocks.
Two details decide whether it works, and both are easy to get wrong.
It has to be hidden without being removed. display: none and visibility: hidden take
it out of the accessibility tree, so a screen reader user never finds it. The working
pattern keeps it in the page and moves it off screen until it is focused.
The target has to be able to receive focus. A <main id="main"> with no tabindex is not
focusable, so several browsers scroll to it and leave focus back on the link. The next Tab
press then goes to the second navigation item, which is the bug the link was there to fix.
How to use
- Set the link text and the id of your main content.
- Pick how it appears. Slide in is the convention; always visible is the most accessible and the one designers reject.
- Paste the link as the first thing inside
<body>, before the header. - Keep the
tabindex="-1"on the target.
Example
<a class="skip-link" href="#main">Skip to main content</a>
<main id="main" tabindex="-1">
<!-- Page content. -->
</main>
with the CSS that reveals it on focus:
.skip-link {
position: absolute;
z-index: 999;
top: 8px;
left: 8px;
padding: 0.75rem 1rem;
background: #1e56ec;
color: #ffffff;
border-radius: 4px;
transform: translateY(calc(-100% - 8px - 0.5rem));
transition: transform 150ms ease-in-out;
}
.skip-link:focus,
.skip-link:focus-visible {
transform: translateY(0);
}
The link is always in the page and always in the accessibility tree. It is moved out of sight with a transform, which costs nothing and reverses on focus.
Pitfalls
Never display: none. It removes the element from the accessibility tree entirely.
The same goes for visibility: hidden and for hidden. A width: 0; height: 0 element is
also unreliable: some browsers will not focus a zero-size element.
The target needs tabindex="-1". It makes the element programmatically focusable
without adding it to the tab order. Skipping it is the single most common reason a skip
link “does nothing”.
Put it first. Before the header, before a cookie banner, before a chat widget. It is only useful if it is the first thing reached, and a banner that traps focus above it defeats it.
Do not remove the focus outline from the link. It is the one element on the page that exists to be focused. Removing the outline for tidiness makes it invisible at the exact moment somebody needs it.
Say where it goes. “Skip” is announced with no destination. “Skip to main content” tells a screen reader user what happens.
One link is usually enough. Sites sometimes add four, to navigation, search, content and footer. Each one is another thing to tab through before reaching the content, which is the problem you started with.
Check it after a route change. In a single-page app, moving to a new route does not reset focus, so the skip link has to be re-focusable and the target has to still exist. A framework that keeps the header mounted and swaps the main element can lose the id.
Compatibility
Everything runs in the browser: nothing is uploaded and nothing is stored.
The generated CSS uses :focus and :focus-visible together. :focus-visible has been in
every current browser since 2022 and is the better selector, since it shows the link for
keyboard focus and not for a click; :focus is kept alongside for older browsers, and for
the case where a script moves focus programmatically.
The slide and fade variants both respect prefers-reduced-motion, which turns the
transition off. That is a real preference on a link that appears suddenly at the top of the
page.
Values you type are escaped for the context they land in, so a colour containing a semicolon and a brace cannot close the declaration and start a new rule: it comes out as inert text.
WordPress block themes: the canonical place for this is a skip-link in the template,
which several themes already ship, and Twenty Twenty-One onwards includes one. Check
whether yours does before adding a second.
Frequently asked questions
Should the link be visible all the time?
What id should I use?
main and content are the usual ones.
The id has to be unique and it has to exist, and the link is only as good as the target.Does it need JavaScript?
href="#main" plus tabindex="-1" on the target is enough in every current browser.
Older WebKit needed a script to move focus, and that has not been necessary for years.Where does it go in a WordPress theme?
<body> in header.php. In a block theme, as the first
block in the header template part, or leave the theme’s own skip link alone if it has one.