Accessible Navigation System — Inclusive Navigation Components
In this tutorial, you will learn about Accessible Navigation System. We cover key concepts, practical examples, and best practices to help you master this topic.
An accessible navigation system provides consistent navigation landmarks, skip links, current page indicators, dropdown menus with keyboard support, responsive patterns, and visible focus indicators for all navigation items.
What You'll Learn
You will learn how to build accessible navigation components for a design system, including skip links, primary navigation, dropdown menus, breadcrumbs, and responsive navigation patterns.
Why It Matters
Navigation is how users find their way around a product. Inaccessible navigation blocks users from accessing content and completing tasks. Navigation must work with keyboard, screen readers, and zoom.
Real-World Use
DodaKit's navigation system includes a skip link at the top, a nav element with aria-label, aria-current for the active page, and dropdown menus with full keyboard support. The navigation collapses to a single column at 400 percent zoom.
flowchart TD A[Navigation System] --> B[Skip Link] A --> C[Primary Navigation] A --> D[Dropdown Menus] A --> E[Breadcrumbs] A --> F[Responsive Pattern] B --> B1[First focusable item] C --> C1[nav + aria-label] D --> D1[Keyboard open/close] E --> E1[aria-current] F --> F1[Hamburger at zoom]
Skip Link
A skip link is the first focusable element on the page. It lets keyboard users skip directly to the main content. It becomes visible when focused.
Primary Navigation
Use the nav element with an aria-label. Use an unordered list for menu items. Mark the current page with aria-current="page".
Dropdown Menus
Dropdown menus must be operable with keyboard. Enter or Space opens the menu. Arrow keys navigate items. Escape closes the menu. Focus returns to the trigger.
// Accessible dropdown navigation
class AccessibleDropdown {
constructor(triggerLabel, items) {
this.id = `dropdown-${Date.now()}`;
this.triggerLabel = triggerLabel;
this.items = items;
this.open = false;
this.focusedIndex = -1;
}
toggle() {
this.open = !this.open;
this.focusedIndex = this.open ? 0 : -1;
return this.getState();
}
openMenu() {
this.open = true;
this.focusedIndex = 0;
return this.getState();
}
closeMenu() {
this.open = false;
this.focusedIndex = -1;
return this.getState();
}
navigate(direction) {
if (!this.open) return this.getState();
if (direction === 'down') {
this.focusedIndex = (this.focusedIndex + 1) % this.items.length;
} else if (direction === 'up') {
this.focusedIndex = (this.focusedIndex - 1 + this.items.length) % this.items.length;
} else if (direction === 'home') {
this.focusedIndex = 0;
} else if (direction === 'end') {
this.focusedIndex = this.items.length - 1;
}
return this.getState();
}
getState() {
return {
id: this.id,
open: this.open,
focusedIndex: this.focusedIndex,
focusedItem: this.open ? this.items[this.focusedIndex] : null,
triggerAria: {
'aria-expanded': this.open,
'aria-haspopup': 'true',
'aria-controls': `${this.id}-menu`
},
menuAria: {
'role': 'menu',
'aria-label': this.triggerLabel,
'hidden': !this.open
}
};
}
render() {
return `<nav aria-label="${this.triggerLabel}">
<button
id="${this.id}-trigger"
class="ds-nav__dropdown-trigger"
aria-expanded="${this.open}"
aria-haspopup="true"
aria-controls="${this.id}-menu"
type="button">
${this.triggerLabel}
<span aria-hidden="true" class="ds-nav__arrow">${this.open ? '\u25B2' : '\u25BC'}</span>
</button>
<ul
id="${this.id}-menu"
class="ds-nav__dropdown-menu"
role="menu"
aria-label="${this.triggerLabel}"
${this.open ? '' : 'hidden'}>
${this.items.map((item, i) =>
`<li role="none">
<a role="menuitem" href="${item.href}"
class="ds-nav__dropdown-item"
tabindex="${this.open && this.focusedIndex === i ? '0' : '-1'}">
${item.label}
</a>
</li>`
).join('\n ')}
</ul>
</nav>`;
}
}
const nav = new AccessibleDropdown('Products', [
{ label: 'Durga Antivirus', href: '/antivirus' },
{ label: 'Doda Browser', href: '/browser' },
{ label: 'DodaZIP', href: '/dodazip' }
]);
console.log(nav.openMenu());
console.log(nav.navigate('down'));
console.log(nav.closeMenu());
Expected output:
{
id: 'dropdown-...',
open: true,
focusedIndex: 0,
focusedItem: { label: 'Durga Antivirus', href: '/antivirus' },
triggerAria: { 'aria-expanded': true, 'aria-haspopup': 'true', 'aria-controls': '...-menu' },
menuAria: { 'role': 'menu', 'aria-label': 'Products', hidden: false }
}
{
id: 'dropdown-...',
open: true,
focusedIndex: 1,
focusedItem: { label: 'Doda Browser', href: '/browser' },
...
}
{
id: 'dropdown-...',
open: false,
focusedIndex: -1,
focusedItem: null,
...
}
Breadcrumbs
Breadcrumbs show the user's location in the site structure. Use nav with aria-label="Breadcrumbs". Mark the current page with aria-current="page". Separators should be aria-hidden.
<!-- Accessible navigation components -->
<nav aria-label="Breadcrumbs" class="ds-breadcrumbs">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li aria-current="page">Durga Antivirus Pro</li>
</ol>
</nav>
<header class="ds-header">
<a href="#main" class="ds-skip-link">Skip to main content</a>
<nav aria-label="Main" class="ds-nav">
<ul class="ds-nav__list">
<li><a href="/" aria-current="page">Home</a></li>
<li class="ds-nav__dropdown">
<button aria-expanded="false" aria-haspopup="true">Products</button>
<ul hidden class="ds-nav__submenu" role="menu">
<li role="none"><a href="/antivirus" role="menuitem">Durga Antivirus</a></li>
<li role="none"><a href="/browser" role="menuitem">Doda Browser</a></li>
</ul>
</li>
<li><a href="/support">Support</a></li>
</ul>
</nav>
</header>
Common Mistakes
1. No Skip Link
Pages without a skip link force keyboard users to Tab through every navigation item before reaching main content.
2. Missing aria-current
Without aria-current, screen reader users cannot easily identify the current page in navigation.
3. Dropdown Menus Without Keyboard Support
Dropdowns that only open on hover or click without keyboard support exclude keyboard users.
4. Mobile Navigation That Is Not Accessible
Hamburger menus that do not announce when open or closed, and that do not trap focus, fail Accessibility.
5. No Landmark Navigation
Navigation must be wrapped in a nav element with an aria-label to be identified as a landmark by screen readers.
6. Navigation Items Without Visible Focus
Every navigation link must have a visible focus indicator. This is critical for keyboard navigation.
7. Dropdown Menus That Hide on Mouse Leave
Menus that close when the mouse leaves are frustrating for users with motor disabilities. Use click-based toggle or Esc to close.
Practice Questions
1. What is the first focusable element on every page?
The skip link. It lets keyboard users jump to the main content.
2. How do you mark the current page in navigation?
Use aria-current="page" on the link or element representing the current page.
3. What keyboard keys should open and close a dropdown menu?
Enter or Space to toggle. Escape to close. Arrow keys to navigate items.
4. Why should hamburger menus be used cautiously?
Hamburger menus hide navigation, increasing cognitive load. Persistent visible navigation is more accessible.
5. Challenge: Create an accessible primary navigation component with a dropdown submenu. Include skip link, keyboard support, and aria-current.
FAQ
Mini Project
Create an accessible navigation system for a design system with skip link, primary nav, dropdown menu, breadcrumbs, and responsive pattern. Document keyboard interactions.
What's Next
Learn about Documentation for a11y standards in design systems. Then explore Pattern Library architecture.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro