Skip links for keyboard users
Skip links are one of the most important accessibility features for keyboard users. They allow users to bypass repetitive navigation and jump directly to the main content.
Why skip links matter
Keyboard users must tab through every focusable element to navigate a page. Without skip links, they must tab through the entire navigation menu on every page load before reaching the main content.
For a site with 20 navigation links, a keyboard user would need to press Tab 20 times on every page just to reach the content. Skip links solve this problem.
Implementing skip links
Here’s how the course website implements a skip link:
< header class = "sticky top-0 z-10 py-3" >
< a href = "#main-content" class = "skip-link" > Skip to content </ a >
< nav class = "container mx-auto flex items-center justify-between gap-4" >
<!-- Navigation content -->
</ nav >
</ header >
< main id = "main-content" tabindex = "-1" >
<!-- Main page content -->
</ main >
Create the skip link
Place a skip link as the first focusable element in your <header>: < a href = "#main-content" class = "skip-link" > Skip to content </ a >
Add ID to main content
Give your <main> element an id that matches the skip link href: < main id = "main-content" tabindex = "-1" >
<!-- Content -->
</ main >
The tabindex="-1" allows the main element to receive focus programmatically without adding it to the natural tab order.
Style the skip link
Make the skip link visible when focused. It can be hidden by default but must appear when keyboard users tab to it: .skip-link {
position : absolute ;
left : -9999 px ;
top : auto ;
width : 1 px ;
height : 1 px ;
overflow : hidden ;
}
.skip-link:focus {
position : static ;
width : auto ;
height : auto ;
overflow : visible ;
/* Add visible styling */
padding : 0.5 rem 1 rem ;
background : #3B82F6 ;
color : white ;
text-decoration : none ;
}
Basic navigation structure
Here’s the navigation structure from the course website:
< header class = "sticky top-0 z-10 py-3" >
< a href = "#main-content" class = "skip-link" > Skip to content </ a >
< nav
class = "container mx-auto flex items-center justify-between gap-4 max-md:flex-wrap [&_a]:p-2"
>
< h1 class = "max-md:mx-auto" >
< a class = "flex items-center gap-2 text-2xl" href = "/" aria-current = "page" >
< img
src = "../../public/tdl-logo.jpg"
width = "30"
height = "30"
class = "inline rounded-full shadow-md"
alt = "TestDevLab"
/>
Web A11y for Devs
</ a >
</ h1 >
< ul class = "flex flex-wrap gap-2 *:min-w-fit max-md:w-full max-md:justify-between" >
< li >< a href = "./introduction" > Introduction </ a ></ li >
</ ul >
</ nav >
</ header >
Key accessibility features
Semantic nav element Using <nav> tells screen readers this is a navigation region, allowing users to jump directly to it.
List structure Using <ul> and <li> provides semantic structure and count information (“list, 1 item”).
Descriptive link text Links have clear, descriptive text like “Introduction” instead of generic “click here”.
Current page indication The aria-current="page" attribute marks the current page for screen reader users.
Current page indication
The aria-current attribute is crucial for helping users understand their location within the site.
Using aria-current
< ul >
< li >< a href = "./introduction" aria-current = "page" > Introduction </ a ></ li >
< li >< a href = "./components" > Components </ a ></ li >
< li >< a href = "./forms" > Forms </ a ></ li >
</ ul >
When a screen reader encounters aria-current="page", it announces something like:
“Introduction, link, current page”
This immediately tells users which page they’re currently viewing.
Values for aria-current
Indicates the current page within a set of pages. < a href = "/introduction" aria-current = "page" > Introduction </ a >
Most common use case for navigation menus.
Indicates the current step in a multi-step process. < li aria-current = "step" > Step 2: Payment </ li >
Useful for checkout flows or wizards.
Indicates the current location within an environment or context. < a href = "#section-2" aria-current = "location" > Section 2 </ a >
Good for table of contents or in-page navigation.
Indicates the current date within a calendar or date picker. < button aria-current = "date" > March 4, 2026 </ button >
Indicates the current time within a time picker. < button aria-current = "time" > 10:30 AM </ button >
Generic current indicator when no specific type applies. < div aria-current = "true" > Current item </ div >
Don’t use aria-current="false" - simply omit the attribute on non-current items.
Keyboard navigation best practices
Tab order matters
Ensure your navigation follows a logical tab order:
Skip link first
The skip link should be the first focusable element: < header >
< a href = "#main-content" class = "skip-link" > Skip to content </ a >
<!-- Then navigation -->
</ header >
Logo and navigation
Then users should be able to tab through the logo and navigation links in a logical order: < nav >
< h1 >< a href = "/" > Logo </ a ></ h1 >
< ul >
< li >< a href = "/introduction" > Introduction </ a ></ li >
< li >< a href = "/course-structure" > Course Structure </ a ></ li >
</ ul >
</ nav >
Main content next
After navigation, focus should move into the main content area.
Never use tabindex values greater than 0. This disrupts the natural tab order and creates a confusing experience. <!-- Bad - don't do this -->
< a href = "/" tabindex = "1" > Link 1 </ a >
< a href = "/" tabindex = "2" > Link 2 </ a >
Focus indicators
All navigation links must have visible focus indicators. From the course material:
“Any interactive element must have a focus indicator to let (keyboard-only) users know where they are on the page.”
You don’t have to use the default outline - there are many creative ways to show focus:
/* Background change on focus */
nav a :focus {
background-color : #3B82F6 ;
color : white ;
outline : none ;
}
/* Box shadow focus indicator */
nav a :focus {
box-shadow : 0 0 0 3 px #3B82F6 ;
outline : none ;
}
/* Transform on focus */
nav a :focus {
transform : scale ( 1.05 );
outline : 2 px solid #3B82F6 ;
outline-offset : 2 px ;
}
If you remove the default outline with outline: none, you MUST provide an alternative visible focus indicator. Never leave interactive elements without a focus indicator.
Multiple navigation regions
When you have multiple <nav> elements on a page, label them to help screen reader users distinguish between them:
< header >
< nav aria-label = "Primary navigation" >
< ul >
< li >< a href = "/" > Home </ a ></ li >
< li >< a href = "/introduction" > Introduction </ a ></ li >
< li >< a href = "/course-structure" > Course Structure </ a ></ li >
</ ul >
</ nav >
</ header >
< main >
<!-- Content -->
</ main >
< footer >
< nav aria-label = "Footer navigation" >
< ul >
< li >< a href = "/privacy" > Privacy </ a ></ li >
< li >< a href = "/terms" > Terms </ a ></ li >
</ ul >
</ nav >
</ footer >
Mobile navigation considerations
For responsive navigation menus that toggle visibility:
< button
aria-expanded = "false"
aria-controls = "mobile-menu"
class = "menu-toggle"
>
< span class = "sr-only" > Open menu </ span >
<!-- Hamburger icon -->
</ button >
< nav id = "mobile-menu" hidden >
< ul >
< li >< a href = "/" > Home </ a ></ li >
<!-- More links -->
</ ul >
</ nav >
Use aria-expanded
Indicates whether the menu is expanded or collapsed: toggleButton . addEventListener ( 'click' , () => {
const expanded = toggleButton . getAttribute ( 'aria-expanded' ) === 'true' ;
toggleButton . setAttribute ( 'aria-expanded' , ! expanded );
menu . hidden = expanded ;
});
Use aria-controls
Links the button to the menu it controls: < button aria-controls = "mobile-menu" > Menu </ button >
< nav id = "mobile-menu" > <!-- Links --> </ nav >
Toggle hidden attribute
Use the hidden attribute to hide/show the menu: menu . hidden = false ; // Show
menu . hidden = true ; // Hide
Manage focus
When the menu opens, optionally move focus to the first link. When it closes, return focus to the toggle button: if ( ! expanded ) {
// Opening menu
menu . querySelector ( 'a' ). focus ();
} else {
// Closing menu
toggleButton . focus ();
}
Real-world example
The course website demonstrates these patterns throughout its navigation structure:
< body class = "min-h-dvh" >
< header class = "sticky top-0 z-10 py-3" >
< a href = "#main-content" class = "skip-link" > Skip to content </ a >
< nav class = "container mx-auto flex items-center justify-between gap-4" >
< h1 class = "max-md:mx-auto" >
< a class = "flex items-center gap-2 text-2xl" href = "/" >
< img
src = "../../public/tdl-logo.jpg"
width = "30"
height = "30"
class = "inline rounded-full shadow-md"
alt = "TestDevLab"
/>
Web A11y for Devs
</ a >
</ h1 >
< ul class = "flex flex-wrap gap-2" >
< li >< a href = "./introduction" aria-current = "page" > Introduction </ a ></ li >
</ ul >
</ nav >
</ header >
< main id = "main-content" tabindex = "-1" >
<!-- Main content here -->
</ main >
< footer class = "py-3 text-center font-semibold" >
< p > Web A11y for Devs, < span id = "current-year" ></ span ></ p >
</ footer >
</ body >
Notice:
Skip link at the top
Semantic <nav> element
List structure for navigation items
aria-current="page" on current page link
Proper landmark elements (header, nav, main, footer)
Descriptive link text
Logo with alt text on image
Best practices checklist
Always provide a skip link as the first focusable element to bypass navigation.
Use <nav>, <ul>, and <li> for navigation menus, not generic divs.
Use aria-current="page" on the current page link in navigation.
Ensure all navigation links have clear, visible focus indicators.
Never use tabindex > 0. Let the DOM order define the tab order.
Use aria-label to distinguish between multiple navigation regions.
Use meaningful link text, not “click here” or “read more”.
Use aria-expanded and aria-controls for mobile menu toggles.
Resources