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.

Enable JavaScript to customise; default output below.

The id on your main content element. It has to exist, and it has to be focusable, which is what the tabindex below is for.

How it appears

Slide in is the convention. Always visible is the most accessible option and the one designers reject.

Any CSS colour. Check the contrast against the text colour: this is a link people need to read in a hurry.

Live preview skip-link.html
<!--
	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.

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

  1. Set the link text and the id of your main content.
  2. Pick how it appears. Slide in is the convention; always visible is the most accessible and the one designers reject.
  3. Paste the link as the first thing inside <body>, before the header.
  4. 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?
It is the most accessible option and it is unpopular with designers. Hidden until focus is the accepted compromise, and it is fine as long as it really is available to assistive technology, which is what the clipping pattern is for.
What id should I use?
Whatever your main content element already has. 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?
No. 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?
In a classic theme, straight after <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.
How do I test it?
Load the page, press Tab once, and look at the top of the viewport. The link should appear and be readable. Press Enter, then Tab again: the next focus should be inside the content, not back in the navigation.
Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.