Skip to content

Accessible Navigation System — Inclusive Navigation Components

DodaTech Updated 2026-06-28 5 min read

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]

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 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 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

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

Should I use role='navigation' or the nav element?

Use the nav element. It is semantically a navigation landmark. role='navigation' is only needed if you cannot use the native element.

How many navigation landmarks should a page have?

Typically one or two: primary navigation in the header and breadcrumb navigation. Too many landmarks create noise.

Do tablet and mobile users need navigation landmarks?

Yes. All users benefit from navigation landmarks, regardless of device.

How do I handle mega menus accessibly?

Use the same dropdown pattern but organize items with headings and links. Ensure keyboard navigation works across all sections.

Should navigation items be buttons or links?

Use links for navigation to other pages. Use buttons for actions like opening a menu or search.

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