Skip links need a target that exists
- Rule:
axe/skip-link - Checks: The skip-link target should exist and be focusable
- WCAG: none; a best practice, not a WCAG success criterion
- Fix class:
mechanical: A fix needs no authored text or visual decision.
Who it affects
Section titled “Who it affects”- People who use a keyboard or a switch device instead of a mouse.
- People who use a screen reader or a braille display, most of them blind.
How to fix it
Section titled “How to fix it”The skip link’s href names an id that is not on the page
Section titled “The skip link’s href names an id that is not on the page”Point the link at the id of the element where the main content starts, usually <main>, spelled with the same case. Give the target tabindex="-1" if it is not focusable, so focus moves with the scroll.
<a class="skip-link" href="#main">Skip to main content</a><main id="main" tabindex="-1"> <h1>Order history</h1></main>The target exists but is hidden when the link is used
Section titled “The target exists but is hidden when the link is used”axe asks for review. Show the target before focus moves to it: an element hidden with display: none or the hidden attribute cannot take focus.
Fixes that fail
Section titled “Fixes that fail”Hiding the skip link with display: none or visibility: hidden
Section titled “Hiding the skip link with display: none or visibility: hidden”It can then never take focus, so no one can use it. Hide it with a class that moves or clips it, and show it on :focus.
In React
Section titled “In React”The part of the fix that differs in React, in its syntax. The rest of the guidance holds.
The skip link targets an id inside route content that is not always mounted
Section titled “The skip link targets an id inside route content that is not always mounted”Put the id on the <main> of the layout component every route renders inside, so the target exists on every page, even while route content loads.
function Layout({ children }) { return ( <> <a className="skip-link" href="#main"> Skip to main content </a> <SiteHeader /> <main id="main" tabIndex={-1}> {children} </main> </> );}The app uses hash-based routing, so href="#main" changes the route
Section titled “The app uses hash-based routing, so href="#main" changes the route”Keep the href, but handle the click: prevent the default and move focus to the target yourself, so the router never sees the hash.
Fixes that fail in React
Section titled “Fixes that fail in React”Scrolling the target into view in the click handler without focusing it
Section titled “Scrolling the target into view in the click handler without focusing it”The page moves, but focus stays on the skip link, so the next Tab still goes through the navigation.
More on the rule
Section titled “More on the rule”axe-core’s documentation of the rule
CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.