UXR SEO Analyzer documentation

Detailed guide

Consistent Navigation Implementation Guide

View contents

Introduction

Consistent navigation is a Level AA accessibility requirement defined in WCAG 2.2 under success criterion 3.2.3. The criterion states that navigation mechanisms repeated across multiple pages within a site must appear in the same relative order every time they are presented, unless the user initiates the change.

As people move through a site, they build a mental model of where things are. A menu whose order changes between pages forces them to re-learn the interface on every visit, which is especially costly for people with cognitive disabilities, for screen reader users who memorise the structure, and for people with motor disabilities who rely on muscle memory to navigate efficiently.

Implementing consistent navigation takes architectural decisions: centralise the menu definition, guarantee the same order on every page, and hold the visual position stable regardless of what each page contains.

Why navigation consistency matters

The cost of inconsistent navigation goes well beyond compliance. Usability studies put task completion between 40% and 60% slower when navigation changes position or order between pages. For assistive technology users the impact is larger still: a screen reader walks elements sequentially, so when the menu structure shifts, the user loses their point of reference.

The mistakes that break consistency most often are:

  • Dynamic reordering: Algorithms that reorder menu items by popularity or by the context of the current page.
  • Selective hiding that does not preserve order: Removing menu items on certain pages in a way that shifts the relative position of the remaining ones.
  • Different navigation on mobile and desktop: Presenting items in a different order in the hamburger menu than in the desktop menu.
  • Injecting dynamic items: Adding promotional or contextual items that displace the existing ones.

The underlying rule is simple: items may be hidden, but the ones that stay visible must always keep the same relative order.

Single source of truth pattern

Consistent navigation starts with defining the menu structure in one place and having every interface component consume it from there.

Central navigation definition

This pattern uses a configuration file as the canonical source. Any change to the navigation structure happens in that file and nowhere else, which is what guarantees every page reflects the same structure.

// navigation.config.ts - single source of truth
export interface NavItem {
  id: string;
  label: string;
  href: string;
  icon?: string;
  children?: NavItem[];
  requiresAuth?: boolean;
  roles?: string[];
}

export const MAIN_NAVIGATION: NavItem[] = [
  { id: 'home', label: 'Home', href: '/' },
  {
    id: 'products',
    label: 'Products',
    href: '/products',
    children: [
      { id: 'electronics', label: 'Electronics', href: '/products/electronics' },
      { id: 'clothing', label: 'Clothing', href: '/products/clothing' },
      { id: 'accessories', label: 'Accessories', href: '/products/accessories' }
    ]
  },
  { id: 'about', label: 'About', href: '/about' },
  { id: 'contact', label: 'Contact', href: '/contact' }
];

The important part of this pattern is that the array defines the canonical order. No component should reorder these items. Components may filter — hiding items by authentication or role — and JavaScript's filter() preserves the original array order, so consistency comes for free.

The navigation service wraps the filtering logic so that items can never be reordered:

export class NavigationService {
  // Filters by authorisation but NEVER reorders
  getMainNavigation(userRoles: string[] = []): NavItem[] {
    return this.filterByAuthorisation(MAIN_NAVIGATION, userRoles);
  }

  private filterByAuthorisation(items: NavItem[], roles: string[]): NavItem[] {
    return items.filter(item => {
      if (!item.roles) return true;
      return item.roles.some(role => roles.includes(role));
    }).map(item => ({
      ...item,
      children: item.children
        ? this.filterByAuthorisation(item.children, roles)
        : undefined
    }));
  }
}

Note that it uses filter() and map(), never sort(). That restriction is deliberate: a developer who needs a different order has to change the source array in navigation.config.ts, not the filtering logic.

Correct semantic markup

A <nav> element with a descriptive aria-label is what lets screen readers identify navigation regions. When a site has several navigation blocks — main, utility, footer — each needs its own unique aria-label to be told apart.

export function MainNavigation() {
  const { mainNav, currentPath } = useNavigation();

  return (
    <nav aria-label="Main navigation">
      <ul role="menubar">
        {mainNav.map(item => (
          <li key={item.id} role="none">
            <a
              href={item.href}
              role="menuitem"
              aria-current={item.href === currentPath ? 'page' : undefined}
            >
              {item.label}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

The markup decisions that matter:

  • aria-current="page" marks the active page both visually and for assistive technology, without touching the menu structure.
  • role="menubar" and role="menuitem" give the menu semantics screen readers recognise as a navigation pattern.
  • role="none" on the <li> strips the list semantics so the screen reader treats the links directly as menu items.

Consistency between desktop and mobile

One of the most common failures is presenting navigation items in a different order on mobile. The hamburger menu must contain exactly the same items in the same order as the desktop navigation. Only the visual presentation changes — never the content or the sequence.

export function useResponsiveNavigation() {
  const [isMobile, setIsMobile] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);

  // CRITICAL: same items, same order in both versions
  // 'navigation' is always MAIN_NAVIGATION
  const navigation = MAIN_NAVIGATION;

  return { isMobile, menuOpen, navigation };
}

The mobile menu button needs its accessibility attributes wired correctly:

  • aria-expanded reports whether the menu is open or closed.
  • aria-controls ties the button to the navigation panel it controls.
  • aria-label gives an accessible name that changes with state ("Open menu" / "Close menu").

CSS for consistent position

The menu's visual position has to be consistent too. A menu that sits at the top on some pages and to the side on others violates the spirit of 3.2.3. Use position: sticky to anchor the navigation and CSS order to keep the layout predictable:

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: white;
  border-bottom: 1px solid #e5e5e5;
}

/* Navigation always in the same relative position */
.main-nav { order: 2; }
.utility-nav { order: 3; }

/* Mobile: change the layout, keep the internal order */
@media (max-width: 767px) {
  .main-nav .nav-list {
    flex-direction: column;
  }
}

Automated consistency testing

Consistent navigation is one of the easiest accessibility criteria to automate. The tests need to cover three things: same item order, same visual position, and a match between the desktop and mobile versions.

test('navigation order is identical across pages', async ({ page }) => {
  const pages = ['/', '/products', '/about', '/contact'];
  const orders = [];

  for (const url of pages) {
    await page.goto(url);
    const items = await page
      .locator('nav[aria-label="Main navigation"] a')
      .allTextContents();
    orders.push(items);
  }

  // Compare every page against the first
  for (let i = 1; i < orders.length; i++) {
    expect(orders[i]).toEqual(orders[0]);
  }
});

test('mobile navigation matches desktop order', async ({ page }) => {
  await page.setViewportSize({ width: 1200, height: 800 });
  await page.goto('/');
  const desktopNav = await page
    .locator('nav[aria-label="Main navigation"] a')
    .allTextContents();

  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('/');
  const menuButton = page.locator('[aria-controls="mobile-menu"]');
  if (await menuButton.isVisible()) await menuButton.click();
  const mobileNav = await page
    .locator('nav[aria-label="Main navigation"] a')
    .allTextContents();

  expect(mobileNav).toEqual(desktopNav);
});

These Playwright tests belong in the CI pipeline, where they catch regressions automatically every time the navigation structure changes.

Checklist

  • [ ] Navigation defined in a single source of truth (a centralised configuration file)
  • [ ] The same navigation items present on every page
  • [ ] The same relative order kept on every page
  • [ ] Navigation's visual position consistent between pages
  • [ ] Mobile navigation in the same order as the desktop version
  • [ ] Only user-initiated changes alter the navigation
  • [ ] Every <nav> block has a descriptive, unique aria-label
  • [ ] aria-current="page" marks the active page
  • [ ] Automated tests verify order and position consistency

Frequently asked questions

Can I hide menu items based on user role?

Yes. Hiding items is acceptable as long as the remaining ones keep their original relative order. Use filter() rather than sort() so the source array's order is preserved.

Yes. It applies to any navigation mechanism repeated across multiple pages, including main navigation, utility navigation, breadcrumbs and footer links.

What if my mobile menu groups items differently?

Grouping visually is allowed — accordions, for instance — but the order of items within each group and between groups must match the desktop version. The content and the sequence do not change; only the presentation does.

How do I handle navigation items that appear only in certain sections?

Contextual submenus, such as a section sidebar, are acceptable as long as the global main navigation stays consistent. Contextual items should be clearly separated with a distinct aria-label.


References

  1. W3C - WCAG 2.2 SC 3.2.3 Consistent Navigation
  2. W3C - G61 Presenting repeated components in the same order
  3. W3C - Navigation landmark pattern
  4. WebAIM - Skip Navigation Links
  5. MDN - Navigation role

Related articles

Related version

Introduction

Consistent Navigation

WCAG 3.2.3 (Consistent Navigation) requires that navigational mechanisms that are repeated on multiple pages within a set of web pages occur in the same...

Category hub

Hub

WCAG 2.2 Compliance Hub

Web accessibility ensures that websites and applications can be used by everyone, including people with disabilities

Last updated: