Navigation
Main nav
The main navigation of a site should follow this basic outline:
<header>
<a href="#main-nav" class="skip-link">Skip to navigation</a>
<a href="#">Website logo</a>
<a href="#">another link</a>
<nav id="main-nav" aria-label="Main">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#" aria-current="page">Products</a></li>
<li><a href="#">Team</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
</header>
- You don’t have to add
role="list"
to the navigation ul because the<nav>
element maintains any native semantic information for list children, regardless of the styles. - The skip link lets users skip over the interactive elements between the skip link and the target. Here, the target is the main navigation. Use this if there are more than two interactive elements before the main navigation.
aria-current
Indicates to the screen reader which page (or element in a set) the user is on. This is a good alternative to class="active"
, but you can use both if you’d like.
aria-current
accepts the following values, each described in detail in the MDN docs:
page
step
location
date
time
true
false
Use the aria-current
selector to style the current page:
a {
border-block-end: 3px solid var(--border-color, transparent);
color: var(--text);
display: inline-block;
font-size: 1.4rem;
padding: 0.5em;
text-decoration: none;
}
a:focus-visible {
outline: 0.25em solid currentColor;
outline-offset: 0.125em;
}
[aria-current="page"] {
--border-color: var(--highlight);
--text: var(var(--highlight));
}
Lists
The <list>
element has an implicit role of list
. When a screen reader encounters a list
, it announces how many items are in the list.
Because all lists have an implicit role of list
, you might think that you don’t have to explicitly add the list
role. However, if you remove the list discs or change any other visual indicators that identify the element and its children as a list, you should include role="list"
.
Alternately, you could set list-style-type: "";
. This doesn’t remove the semantic information from the list.
If you wrap the list in a
<nav>
element, you do not have to add thelist
role.
Examples
Hamburger for narrow viewports
This involves the following steps:
- Create styles for narrow viewports
- Hide the nav list in a sidebar
- Let users toggle its visibility
This example did not work at all, but here is are the styles to figure out later:
HTML
<header>
<nav aria-label="Main" id="main-nav">
<ul id="main-nav-list">
<li><a href="#">Home</a></li>
<li><a href="#" aria-current="page">Products</a></li>
<li><a href="#">Team</a></li>
<li><a href="#">Contact</a></li>
</ul>
<template id="burger-template">
<button
type="button"
aria-expanded="false"
aria-label="Menu"
aria-controls="main-nav-list"
>
<svg viewBox="-5 0 10 8" width="40" aria-hidden="true">
<line
y2="6.5"
stroke="#000"
stroke-width="10"
stroke-dasharray="1.5 1"
/>
</svg>
</button>
</template>
</nav>
</header>
<main role="Main">
...
</main>
CSS
nav {
position: var(--nav-position), fixed;
inset-block-start: 1rem;
inset-block-end: 1rem;
z-index: 1; /* added */
}
ul {
display: flex;
flex-direction: var(--nav-list-layout, column);
flex-wrap: wrap;
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
nav ul {
background: hsl(0 0% 100%);
box-shadow: var(--nav-list-shadow, -5px 0 11px 0 hsl(0 0% 0%/0.2));
display: flex;
flex-direction: var(--nav-list-layout, column);
flex-wrap: wrap;
gap: 1rem;
height: var(--nav-list-height, 100dvh);
list-style: none;
margin: 0;
padding: var(--nav-list-padding, 2rem);
position: var(--nav-list-position, fixed);
inset-block-start: 0;
inset-block-end: 0;
width: var(--nav-list-width, min(22rem, 100vw));
visibility: var(--nav-list-visibility, hidden);
}
[aria-expanded="true"] + ul {
--nav-list-visibility: visible;
}
@media (min-width: 48em) {
nav {
--nav-position: static;
--nav-button-display: none;
}
nav ul {
--nav-list-layout: row;
--nav-list-position: static;
--nav-list-padding: 0;
--nav-list-height: auto;
--nav-list-width: 100%;
--nav-list-shadow: none;
--nav-list-visibility: visible;
}
}
nav ul:first-child {
--nav-list-layout: row;
--nav-list-position: static;
--nav-list-padding: 0;
--nav-list-height: auto;
--nav-list-width: 100%;
--nav-list-shadow: none;
--nav-list-visibility: visible;
}
nav button {
all: unset;
display: var(--nav-button-display, flex);
position: relative;
z-index: 1;
}
button:focus-visible {
outline: 0.25em solid currentColor;
outline-offset: 0.125em;
}
ul {
visibility: var(--nav-button-display, hidden);
}
JS
const nav = document.querySelector('nav');
const list = nav.querySelector('ul');
const burgerTemplate = document.querySelector('#burger-template').content;
const burgerClone = burgerTemplate.cloneNode(true);
const button = burgerClone.querySelector('button');
nav.addEventListener('keyup', e => {
if (e.code === 'Escape') {
button.setAttribute('aria-expanded', false);
button.focus();
}
});
button.addEventListener('click', e => {
const isOpen = button.getAttribute('aria-expanded') === true;
button.setAttribute('aria-expanded', !isOpen);
});
nav.insertBefore(burgerClone, list);