WCAG 3.2.3 Consistent Navigation — Testing Guide for Mobile & Web Apps

WCAG 3.2.3 Consistent Navigation (Level AA) requires that navigational mechanisms that are repeated on multiple pages within a set of web pages occur in the same relative order each time they are repe

By · March 19, 2026 · 20 min read · WCAG Guides

Understanding WCAG 3.2.3 Consistent Navigation

WCAG 3.2.3 Consistent Navigation (Level AA) requires that navigational mechanisms that are repeated on multiple pages within a set of web pages occur in the same relative order each time they are repeated, unless a change is initiated by the user. The same principle applies to mobile apps: any UI component that serves as a navigation aid (menus, tabs, drawers, bottom bars, etc.) must appear in a predictable location and order across screens that belong to the same logical flow or screen set.

In plain language, if a user learns where to find the main menu on one screen, they should be able to find it in the same spot on every other screen that is part of the same app or site, unless they deliberately choose to hide or reorder it. This predictability reduces cognitive load, helps users with memory or attention impairments, and supports people who rely on screen readers or voice control to build a mental model of the interface.

Who benefits?

Real‑world impact when the rule is broken: a user may miss a primary navigation item, become stuck in a dead‑end flow, or trigger an unintended action because a button they expected in the footer appears in the header on a particular screen. In e‑commerce, this can lead to abandoned carts; in banking apps, it can cause failed transactions; in healthcare portals, it may prevent timely access to critical information.

Legal relevance: WCAG 3.2.3 is referenced by the EU European Accessibility Act (EAA) and its harmonized standard EN 301 549, as well as by the Americans with Disabilities Act (ADA) Title III interpretations that treat WCAG conformance as evidence of reasonable accommodation. Non‑conformance can expose organizations to litigation, fines, or loss of public‑sector contracts.

---

Formal Requirement Text

> 3.2.3 Consistent Navigation: Navigational mechanisms that are repeated on multiple Web pages within a set of Web pages occur in the same relative order each time they are repeated, unless a change is initiated by the user.

The definition is deliberately narrow: it does not demand identical visual styling, only the same order. However, most accessibility practitioners treat a change in visual placement (e.g., moving a bottom navigation bar to the top on a specific screen) as a violation because it changes the perceived order for users who rely on spatial memory.

---

Common Violations – Mobile & Web

Web Examples

ViolationDescriptionTypical CauseExample Impact
Header repositionPrimary site header appears at the top on most pages but shifts to the bottom on a promotional landing page.A/B test or special‑case landing page template.Keyboard users tab through unexpected content; screen‑reader users lose the banner landmark.
Conditional footerFooter navigation links appear only when a user is logged in; anonymous pages lack the footer.Feature‑gate logic that removes the footer entirely.Users cannot reach “Help” or “Terms” from logged‑out pages, causing confusion.
Dynamic side‑nav orderA sidebar menu reorders its items based on recent usage (e.g., “Profile” moves to top after visiting).Personalization script that manipulates DOM order.Users who rely on muscle memory cannot locate “Settings” quickly.
Inconsistent breadcrumbBreadcrumb trail present on product pages but omitted on category pages.CMS template oversight.Users lose orientation when navigating back up the hierarchy.

Mobile Examples

ViolationDescriptionTypical CauseExample Impact
Bottom nav hides on scrollA bottom navigation bar disappears when scrolling down a list and reappears only at the top.“Hide on scroll” UI pattern intended to maximize content area.Users who rely on the bar for quick switches lose it mid‑task; voice‑over focus jumps unpredictably.
Drawer vs. tab bar mixSome screens use a navigation drawer, others use a tab bar, with no clear pattern.Feature teams implement preferred pattern without central UI guideline.Users must relearn where to find “Home” on each screen.
In‑app toast replaces navA full‑width toast covers the bottom navigation temporarily, but the toast does not shift focus away from the nav.Implementation uses a system overlay without adjusting accessibility traits.TalkBack users may activate a nav item unintentionally while trying to dismiss the toast.
Language switcher movesA language picker appears in the header on Android but in the settings drawer on iOS for the same app.Platform‑specific design decisions not aligned.Bilingual users lose quick access to language change.
Modal obscures navA modal dialog appears centered, covering the tab bar, but the modal does not trap focus inside it.Missing focus‑management code.Keyboard or switch users tab behind the modal, losing context.

---

Manual Test Steps

Preparation

  1. Identify the navigation set – Determine which UI components are considered “repeated navigational mechanisms” for the product. Common candidates: header/global nav, footer, side drawer, bottom navigation, tab bar, breadcrumb, skip links, and any persistent floating action button (FAB) that serves as a navigation shortcut.
  2. Create a reference map – On a baseline screen (e.g., the home screen or a generic content page), record the order, type, and location of each mechanism. Screenshots or a simple spreadsheet help.
  3. Select user personas – Choose at least three personas that rely on consistency:

Execution

StepActionExpected ResultTools/Notes
1Navigate to the baseline screen.Record the reference order.Use a screen‑reader (TalkBack, VoiceOver, NVDA) to hear the sequence.
2Move to a second screen that should share the same navigational set (e.g., a product detail page).The same mechanisms appear in the same order.Visually inspect; confirm with accessibility inspector (Android Studio, Xcode Accessibility Inspector, Chrome DevTools).
3Repeat step 2 for all screens in the set (including error states, empty states, modal backgrounds).No deviation unless a user‑initiated change (e.g., a user toggles a sidebar).Log any deviations with screenshot and screen name.
4Test keyboard navigation: press Tab repeatedly.Focus moves through nav items in the reference order, never jumps out of order or skips items.Use a visible focus outline; ensure no hidden elements receive focus.
5Test screen‑reader navigation: swipe or use rotor to jump to headings/landmarks.Announcements follow the reference order; no unexpected items appear.Note any extra announcements (e.g., a toast that appears as a navigation item).
6Test switch or voice control: issue a command to activate a known nav item (e.g., “Go to Settings”).The command succeeds on every screen where the item is present.If the item is missing or misplaced, the command fails or triggers the wrong action.
7Test user‑initiated change: open a settings pane that allows the user to hide or reorder nav items.After the change, the new order is persisted and reflected on all subsequent screens until the user changes it again.Verify persistence across app restarts or page reloads.
8Document findings.Each deviation is logged as a potential WCAG 3.2.3 failure with severity (usually AA).Attach steps to reproduce, expected vs. actual order, and impacted personas.

Edge‑Case Checks

---

Automated Tools That Check This Criterion

Web

ToolHow It Checks 3.2.3Integration PointLimitations
axe‑core (Deque)Provides a rule aria-allowed-attr and custom rules via axe.configure({rules: { 'navigation-order': { enabled: true } }}). Community‑contributed rule sets (e.g., axe-plugin-navigation-order) compare the DOM order of repeated landmarks across pages in a crawl.CI pipeline (npm script), Jest‑axe, Cypress‑axe, Playwright‑axe.Requires a defined set of pages to compare; does not detect visual reposition if DOM order unchanged but CSS moves items.
WAVE (WebAIM)Flags “Missing consistent navigation” when a header/footer landmark appears in a different DOM position across scanned pages.Browser extension, manual testing, API for bulk scans.Visual‑only shifts may be missed; relies on correct landmark tagging.
LighthouseAccessibility audit includes a check for “Navigation elements are in a consistent order” based on a sampling of pages during the audit.Chrome DevTools, CI via lighthouse-ci.Sampling may miss edge cases; not exhaustive for large sites.
ESLint plugin eslint-plugin-jsx-a11yRule anchor-is-valid and no-noninteractive-tabindex help avoid accidental reordering via tabindex; custom rule can enforce that nav elements appear only once per page or maintain a set order via AST traversal.Pre‑commit hooks, CI lint stage.Only works on JSX/HTML source; cannot catch runtime DOM changes caused by JavaScript unless those changes are statically analyzable.
Storybook accessibility addonRenders each story and runs axe; if a story introduces a nav component in a different location, the addon flags it.Component‑level testing during development.Does not test assembled pages; relies on component isolation.

Mobile

ToolHow It Checks 3.2.3Integration PointLimitations
Android Accessibility Test Framework (ATF)Custom UIAutomator test can iterate through all activities/fragments, collect the IDs of NavigationView, BottomNavigationView, Toolbar, etc., and assert that the sequence matches a baseline list.Instrumented test suite, run via Gradle connectedAndroidTest.Requires maintaining a baseline list; dynamic UI (e.g., conditional visibility) needs extra logic.
iOS UI Test with XCTestUse XCUIApplication to query navigationBars, tabBars, toolbars, and compare their frame.origin or accessibilityElements order across screens.CI via xcodebuild test.Sensitive to safe‑area changes; must account for orientation.
Google Accessibility Scanner (Android)Scans screens for missing content descriptions and touch target size; does not directly test order, but can be combined with a custom script that logs the hierarchy and compares order.Manual or CI via adb shell am broadcast -a com.google.android.apps.accessibility.scanner.SCAN.No built‑in order rule; needs post‑processing.
Apple Accessibility Inspector (Xcode)Allows manual inspection of the accessibility hierarchy; can export the tree for diff‑based order checking.Manual QA; can be scripted via xcrun simctl io booted screenshot and xcrun simctl spawn booted log collect.Export format is verbose; requires parsing.
Espresso Idling Resources + custom matcherCreate a matcher that verifies that a specific View with ID nav_home always appears at index 0 within its parent LinearLayout across multiple fragment transactions.Instrumented tests.Works best when nav is implemented as static view hierarchy; custom view containers need bespoke matchers.
Detox (React Native)Similar to Espresso; can assert that the navigation component’s testID appears in a predictable order within the root view.End‑to‑end test suite for RN apps.Requires consistent testID usage; does not catch native‑only navigation changes.

> Tip: Combine automated checks with a visual regression tool (e.g., Percy, Applitools) to catch cases where DOM order is correct but CSS visually moves items (e.g., order: 2 in Flexbox).

---

Mobile‑Specific Considerations

Android

  1. Navigation Component & Jetpack Navigation – If you use the Navigation component, the NavHost hosts a single <fragment> tag. The bottom nav or drawer is usually defined in the activity layout. Ensure that the activity layout does not change between navigation graph destinations unless the user explicitly opens a settings screen that toggles the nav visibility.
  2. BottomNavigationView – By default, it stays pinned. If you implement setVisibility(View.GONE) on scroll, provide an accessibility workaround: announce the change via AccessibilityEvent.TYPE_VIEW_FOCUSED and keep a persistent “show nav” action accessible via a corner button or voice command.
  3. Navigation Drawer – The drawer should be opened via a consistent gesture (swipe from edge) or a persistent hamburger button. Avoid showing the drawer only on certain screens; if a screen needs full‑width content, consider a modal bottom sheet instead of hiding the drawer.
  4. Toolbar / AppBar – The title and action items may change, but the Toolbar itself should remain at the same vertical position (typically top). Use android:fitsSystemWindows="true" to keep it stable across different screen cutouts.
  5. Dynamic Feature Modules – When a feature module is loaded at runtime, verify that it does not inject its own nav bar that duplicates or reorders the existing one. Use the ComponentActivity’s setContentView only once; subsequent modules should add views inside existing containers.

iOS

  1. UITabBarController – The tab bar should be present in all view controllers that belong to the tab bar’s scope. If you hide it (tabBar.isHidden = true) for a specific view controller, provide an alternative way to reach the same destinations (e.g., a navigation bar button) and ensure the hidden state is user‑initiated (like a settings toggle).
  2. UINavigationBar – The navigation bar is generally consistent; however, custom UINavigationBar subclasses that adjust frame.origin.y based on content size can cause a shift. Stick to the system bar or use prefersLargeTitles which preserves the bar’s position while adjusting title size.
  3. Side Menu (Slide‑out) – Many apps implement a custom side menu using a UIViewController containment pattern. Ensure the menu’s presenting view controller does not alter the underlying navigation stack order.
  4. Gesture‑Based Navigation – The system swipe‑to‑go‑back gesture should not interfere with a persistent tab bar. If you disable the gesture (interactivePopGestureRecognizer.isEnabled = false) on certain screens, make sure the back button in the navigation bar remains accessible and in the same location.
  5. Accessibility Traits – For custom nav buttons, set isAccessibilityElement = true and assign a meaningful accessibilityLabel. Avoid assigning the navigationBar trait to non‑nav views, as this can confuse VoiceOver’s rotor.

Cross‑Platform (React Native, Flutter, Xamarin)

---

Fixing Violations – Code Examples

Web – Header Reposition

Problem: A promotional page moves the site header to the bottom via a CSS class .promo-header { position: fixed; bottom: 0; }.

Fix: Keep the header in the same DOM location; use a visually distinct promo banner instead of repositioning the header.


<!-- Before (incorrect) -->
<body>
  <main>…</main>
  <header class="site-header promo-header">…</header>
</body>

<!-- After (correct) -->
<body>
  <header class="site-header">…</header>
  <main>
    <section class="promo-banner">…</section>
    …
  </main>
</body>

If a visual change is truly needed, ensure the heading order stays the same and add aria-label="Site header" to the header so assistive tech knows its purpose regardless of visual placement.

Web – Conditional Footer

Problem: Footer rendered only when user.isLoggedIn.

Fix: Always render the footer, but hide links that require authentication via aria-disabled="true" or visually hide them with CSS while keeping them in the DOM for screen readers.


<footer>
  <nav aria-label="Footer">
    <ul>
      <li><a href="/help">Help</a></li>
      <li><a href="/terms">Terms</a></li>
      <li><a href="/login" aria-disabled="{!user.isLoggedIn}">Log In</a></li>
    </ul>
  </nav>
</footer>

Add CSS:


footer a[aria-disabled="true"] {
  pointer-events: none;
  opacity: 0.5;
}

Android – Bottom Nav Hide on Scroll

Problem: Using bottomNavigationView.setVisibility(View.GONE) when scrolling a RecyclerView.

Fix: Keep the bottom nav visible at all times. If screen real estate is critical, use a collapsible toolbar that shrinks but does not remove the nav.


// In Activity or Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // Do NOT hide bottom nav on scroll
    // bottomNavigationView.visibility = View.GONE   // <-- remove this
    
    // Instead, enable scrolling behavior on the AppBarLayout
    val appBarLayout = view.findViewById<AppBarLayout>(R.id.app_bar)
    val scrollingView = view.findViewById<RecyclerView>(R.id.recycler_view)
    appBarLayout.setScrollingBehavior(scrollingView)
}

If you must hide it for a specific full‑screen dialog, ensure the dialog is a separate DialogFragment that does not sit underneath the nav; the nav stays visible behind the dialog (or you show a temporary “close” button in the dialog’s header).

iOS – Drawer vs. Tab Bar Mix

Problem: Some view controllers present a UITabBarController, others present a custom side‑menu controller.

Fix: Choose one primary navigation pattern for the app and stick to it. If a side menu is needed for certain sections, implement it as a modal presentation that appears *over* the tab bar, not as a replacement.


// In AppDelegate or SceneDelegate
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let tabBarController = UITabBarController()
    // set up tabs…
    window?.rootViewController = tabBarController
    return true
}

// When presenting side menu:
func showSideMenu() {
    let menuVC = SideMenuViewController()
    // Present as a form sheet over the existing tab bar
    menuVC.modalPresentationStyle = .formSheet
    present(menuVC, animated: true, completion: nil)
}

If you must hide the tab bar for a specific screen (e.g., a splash or onboarding), do it only for those screens and restore it immediately after the user leaves them.

Flutter – Conditional hiding should be user‑initiated (e.g., a “Get Started” button that transitions to the main app).

Web – Dynamic Side‑Nav Order

Problem: JavaScript reorders <li> items based on recent clicks.

Fix: Store the order in a persistent setting (e.g., localStorage) and apply it on page load *once*; do not change the order during a session unless the user explicitly opens a settings pane to reorder.


// nav.js
document.addEventListener('DOMContentLoaded', () => {
    const nav = document.getElementById('main-nav');
    const savedOrder = JSON.parse(localStorage.getItem('navOrder') || '[]');
    if (savedOrder.length) {
        savedOrder.forEach(id => {
            const item = document.getElementById(id);
            if (item) nav.appendChild(item); // moves item to end preserving saved order
        });
    }
});

// Only allow reorder via settings
document.getElementById('reorderBtn').addEventListener('click', () => {
    // open a modal that lets user drag & drop; on save, persist new order
});

Mobile – Language Switcher Position

Problem: Android shows language switcher in the toolbar; iOS hides it in Settings.

Fix: Provide a consistent entry point—for example, a floating action button (FAB) labeled “Language” that appears on both platforms and opens a language selection dialog. Keep the platform‑specific entry as a shortcut but do not rely on it as the sole method.


<!-- Android layout -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/fab_language"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_language"
    android:contentDescription="@string/change_language"
    app:layout_anchorGravity="bottom|end" />

// iOS SwiftUI
struct ContentView: View {
    var body: some View {
        VStack {
            // main content
        }
        .overlay(
            Button(action: { showLanguagePicker() }) {
                Image(systemName: "globe")
                    .accessibilityLabel("Change language")
            }
            .padding()
            , alignment: .bottomTrailing
        )
    }
}

---

Autonomous Persona‑Driven Exploration – How SUSA Checks This Criterion

SUSA (SUSATest) is an autonomous QA platform that explores an app or web property without pre‑written scripts. It builds a model of the UI by interacting with elements as different personas (curious, impatient, novice, adversarial, elderly, accessibility, power user, etc.). Each persona has a configured behavior profile that influences timing, input patterns, and decision‑making.

Exploration Process Relevant to 3.2.3

  1. Discovery Phase – SUSA loads the entry point (home screen or URL) and records all *navigation‑related* UI components it can identify via accessibility APIs (Android AccessibilityNodeInfo, iOS AXUIElement, web ARIA roles/landmarks). It creates a navigation signature – an ordered list of component types and their hierarchical positions (e.g., [Header → BottomNav → FloatingActionButton]).
  1. Traversal Phase – For each discovered screen, SUSA repeats a set of actions that mimic each persona:
  1. Comparison Logic – After gathering navigation signatures from *N* screens, SUSA computes a similarity score. If any screen’s signature deviates beyond a tolerance (e.g., a missing component, a different order, or a component appearing/disappearing without a user‑initiated toggle), it flags a potential WCAG 3.2.3 violation.
  1. Persona‑Specific Validation
  1. Learning & Regression – SUSA stores each navigation signature in a knowledge base. On subsequent runs, it compares the current signature to the stored baseline. If a previously flagged issue is resolved, the status updates to PASS; if a new deviation appears, it is logged as a regression. This cross‑session learning reduces flaky results and helps teams track the impact of refactors or A/B tests.

Why This Approach Beats Simple Rule‑Based Checks

Example Output


[Run 2025-11-02] Navigation Consistency Check
---------------------------------------------
Baseline signature (Home): [Header, BottomNav, FAB]
Screen: ProductDetail
  Observed: [Header, FAB]   ← BottomNav missing
  Impact: Power user cannot switch categories without navigating up.
  Verdict: FAIL (WCAG 3.2.3)

Screen: Settings
  Observed: [Header, BottomNav, FAB, SettingsToggle]   ← SettingsToggle is a user‑initiated toggle, allowed.
  Verdict: PASS

The report includes screenshots, the exact accessibility node paths, and suggestions for fixing (e.g., “ensure BottomNav remains visible in all fragments that belong to the main nav set”).

---

Edge Cases That Only Show Up in Production

  1. Lazy‑Loaded Navigation – Some web apps load the footer via an AJAX call after the initial paint. In a staging environment with throttled network, the footer may appear late, causing automated tools that run before the network settles to miss it. In production, users on slow connections may experience a moment where the navigation set is incomplete, violating 3.2.3 for those users.
  1. Conditional Feature Flags – A/B testing frameworks may enable a new navigation variant for a percentage of users. If the flag is evaluated client‑side after DOM construction, some users see the old order while others see the new order, leading to inconsistent experiences.
  1. System UI Overlays – Android’s picture‑in‑picture (PIP) mode or iOS’s slide‑over can temporarily obscure part of the app’s navigation bar. If the app does not adjust the accessible region or provide a way to reach the hidden controls, users relying on the navigation may be stuck.
  1. Dynamic Font Scaling – Users who increase system font size may cause a navigation bar to wrap or shift vertically. If the bar’s height changes enough to push other content out of the viewport, the perceived order can change for screen‑reader users who rely on spatial proximity.
  1. Modal Dialogs that Trap Focus Incorrectly – A modal that announces itself as a dialog but does not move focus inside it can cause the navigation order to appear shifted when a screen‑reader user navigates by heading or landmark.
  1. Gesture Navigation Interference – Android’s gesture navigation (swipe up for home, swipe left/right for recent) can conflict with custom edge‑swipe drawers. If the app consumes the edge gesture, the system gesture may be blocked, forcing users to use an alternative navigation method that may be less discoverable.

---

Practical Checklist for Developers & QA

✅ ItemDescriptionHow to Verify
Nav Set IdentificationList all UI components that serve as repeated navigation aids (header, footer, side nav, bottom nav, tab bar, breadcrumb, skip links, FAB used for navigation).Review design specs; run an accessibility scan to harvest landmarks/roles.
Baseline Order CaptureRecord the order and hierarchy of the nav set on a reference screen (home or generic content page).Screenshot + accessibility tree export; store as JSON or CSV.
Consistency Across ScreensEnsure every screen that belongs to the same logical flow shows the *same* nav set in the *same* order.Manual navigation; automated crawling with SUSA, axe, or custom UIAutomator/XCTest scripts.
User‑Initiated Changes AllowedIf a setting lets the user hide, show, or reorder a nav element, the change must persist and be communicated.Toggle the setting, navigate to other screens, confirm the new order stays until changed again.
No Visual‑Only ReorderingConfirm that CSS transforms, order, flex-direction, or absolute positioning do not change the perceived nav order.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts. New to the category? Start with what autonomous product intelligence & QA means.

Try SUSA Free