WCAG 2.4.5 Multiple Ways — Testing Guide for Mobile & Web Apps

WCAG 2.4.5 Multiple Ways requires that users can locate a web page or screen by at least two different mechanisms. The intent is to give people choice in how they navigate, especially when one method

By · January 22, 2026 · 18 min read · WCAG Guides

Understanding WCAG 2.4.5 Multiple Ways (Plain English)

WCAG 2.4.5 Multiple Ways requires that users can locate a web page or screen by at least two different mechanisms. The intent is to give people choice in how they navigate, especially when one method may be difficult or unavailable due to disability, context, or preference. For a web site, the two mechanisms commonly satisfy the rule are: (1) a set of navigation links (e.g., a global menu or breadcrumb trail) and (2) a search function or a site map. For a mobile app, the same principle applies: users should be able to reach a particular screen via more than one path, such as a bottom navigation bar and a deep link or a searchable list.

The success criterion is Level AA, meaning it is required for conformance under many accessibility laws, including the EU European Accessibility Act (EAA) and the Americans with Disabilities Act (ADA) as interpreted by the Department of Justice. Failure to provide multiple ways can block users who rely on assistive technology, have motor impairments that make precise tapping difficult, or simply prefer a different navigation style.

Who It Affects and Real User Impact

People who benefit from multiple ways include:

When only a single path exists, a failure can manifest as:

These scenarios translate directly into lost conversions, increased support calls, and potential legal risk under the EAA (which mandates WCAG 2.1 AA for public sector websites and mobile apps) and ADA Title III litigation trends.

Common Violations: Web Examples

Violation TypeDescriptionExample CodeWhy It Fails 2.4.5
Hidden navigationMain menu only appears via a hamburger icon that is not keyboard focusable.<button aria-label="Open menu" id="hamburger">☰</button>
<nav id="main-nav" style="display:none;">…</nav>
Users who cannot perceive the icon or cannot activate it (e.g., switch‑control) have no alternative route to top‑level pages.
No search or site mapContent‑heavy site relies solely on hierarchical menus; no search box or sitemap link.<header><nav>…</nav></header> (no <form role="search">)Users who cannot drill down through many levels lack a shortcut to reach deep content.
Deep link only via JavaScriptA feature is reachable only by clicking a dynamically generated button that is not present in the initial DOM.document.getElementById("load-features").addEventListener("click", () => { … })If JavaScript fails or is disabled, the button never appears, leaving a single path (the homepage) to reach the feature.
Breadcrumb missing on internal pagesOnly the homepage shows breadcrumbs; subpages rely solely on the top nav.<ol class="breadcrumb">…</ol> present only on /Users who land on a deep page via search or external link have no way to see the hierarchy or navigate back via a secondary mechanism.

Each of these examples demonstrates a single point of entry to a set of pages, violating the requirement for at least two distinct mechanisms.

Common Violations: Mobile Examples (Android/iOS)

Android

ViolationDescriptionKotlin/XML SnippetFailure Reason
Navigation drawer only reachable by swipe from edgeNo visible indicator or action button to open drawer; TalkBack does not announce the gesture.<androidx.drawerlayout.widget.DrawerLayout …>
android:lockMode="locked_closed" (programmatically opened only by drawerLayout.openDrawer(GravityCompat.START))
Users who cannot perform edge swipes (e.g., due to tremor) have no alternative way to reach drawer destinations.
Bottom navigation hidden behind a modalThe bottom nav is only shown after a tutorial splash; after dismissal it is never displayed again.if (!prefs.getBoolean("tutorial_shown", false)) { showTutorial(); } else { bottomNav.visibility = View.VISIBLE }Once tutorial is dismissed, users relying on the bottom bar lose that navigation method; only the hamburger menu remains.
Deep link only via custom URI scheme not exposed in UIA settings screen is reachable only by clicking a link in an email that uses myapp://settings. No UI element inside the app opens that screen.<intent-filter> <data android:scheme="myapp"/> </intent-filter>Users who never receive the email (or have email client restrictions) lack a second path to the settings screen.
Accessibility service overrides gestures causing loss of alternative navigationA custom service consumes all swipe events, preventing the system navigation bar from being used to go home.override fun onGestureEvent(event: GestureEvent): Boolean { return true }Users who depend on system gestures for navigation lose the ability to exit the app or reach the launcher, leaving only in‑app navigation.

iOS

ViolationDescriptionSwift/SnippetFailure Reason
Tab bar hidden behind a modal onboarding flowAfter onboarding, the tab bar is set to isHidden = true and never restored.if !UserDefaults.standard.bool(forKey: "onboarded") { showOnboarding(); tabBarController?.tabBar.isHidden = true }Users who skip onboarding lose the tab bar, leaving only a sidebar menu that may not be reachable via VoiceOver.
Search bar only appears after pulling down on a listNo visible search icon; the search controller is activated solely by UIRefreshControl‑like gesture.let searchController = UISearchController(...) ; navigationItem.searchController = searchController (but searchController.isActive = false until pull‑down)Users who cannot perform the pull‑down gesture (e.g., due to motor impairment) have no way to invoke search.
Settings reachable only via a 3‑D Touch quick actionOn devices without 3‑D Touch, the quick action menu does not appear; no alternative entry point exists.func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { … }Users on older iPhones or iPads lack any way to open the settings screen.
Custom back gesture overrides system edge swipe, removing alternative exitAn interactive pop gesture is disabled, forcing users to rely solely on a programmatically placed back button that may be off‑screen.navigationController?.interactivePopGestureRecognizer?.isEnabled = falseUsers who depend on the system edge swipe for navigation lose that method, leaving only the custom button which may be inaccessible.

These mobile patterns illustrate how a single gesture‑ or UI‑dependent path can block users who cannot perform that gesture or who rely on assistive technology that does not expose it.

Manual Testing Approach

Testing WCAG 2.4.5 manually involves verifying that each screen or state provides at least two distinct ways to reach it. The process can be broken into three phases: discovery, verification, and documentation.

1. Discovery

2. Verification

For each screen, attempt to reach it using at least two of the identified mechanisms while simulating different user profiles:

ProfileTechniqueWhat to Observe
Screen‑reader userNavigate using TalkBack/VoiceOver; listen for announcements of navigation elements.Ensure each mechanism is announced and activatable.
Motor‑impairment userUse switch control, head tracking, or voice input; avoid precise gestures.Verify that at least one mechanism does not require fine‑grained touch.
Keyboard‑only userTab through interactive elements; use Enter/Space to activate.Confirm that navigation is reachable without mouse.
Cognitive‑load userFollow a simple instruction set (e.g., “go to Settings via the bottom bar, then via search”).Check that the user can complete the task without hesitation.
Power userTry keyboard shortcuts or command palettes.Ensure shortcuts are documented and functional.

If a screen is reachable only via a single mechanism for any profile, flag it as a violation.

3. Documentation

Record findings in a table similar to the one below:

ScreenMechanism 1Mechanism 2Pass/FailNotes
HomeTop navLogo linkPassBoth announced by TalkBack.
SettingsBottom navDeep link (myapp://settings)FailDeep link works but no UI entry; switch control cannot activate bottom nav due to obscured hit‑test.
Product DetailBreadcrumbSearchPassBreadcrumb visible; search returns product.
Checkout ConfirmationOrder history linkEmail linkFailEmail link not present in app; only order history link reachable via bottom nav (which is hidden after first purchase).

Manual testing is time‑consuming but essential for catching context‑specific issues (e.g., a navigation item that disappears after a certain state).

Automated Testing Tools and Techniques

Automated checks can catch many structural violations, especially those related to missing alternative navigation elements in the DOM or view hierarchy. However, automation cannot fully substitute for human judgment about usability; it should be used as a first line of defense.

Web Automation

ToolWhat It ChecksHow to Configure for 2.4.5
axe‑core (via @axe-core/react, axe-cli, or browser extension)Detects missing landmarks, missing search role, missing site map link, missing breadcrumbs.Run axe.run() and filter for rules: region, link-name, heading-order, page-has-heading-one. Add custom rule to ensure at least two distinct navigation sections (nav or [role="navigation"]) are present.
LighthouseAccessibility audit includes “navigation” checks.In CI, run lighthouse --only-categories=accessibility --preset=desktop and assert score ≥ 90 for the “navigation” sub‑score.
pa11yScriptable accessibility testing.Create a test script that asserts document.querySelectorAll('nav, [role="navigation"]').length >= 2 or that a search input (input[type="search"]) exists.
Cypress + cypress-axeEnd‑to‑end test with accessibility assertions.`cy.visit('/')\ncy.injectAxe()\ncy.checkA11y(null, { includedImpacts: ['critical', 'serious'] })` Add a custom cypress command to verify two navigation mechanisms.

Example: Custom axe rule for Multiple Ways


// multiple-ways.js
module.exports = {
  id: 'multiple-ways',
  description: 'Page must provide at least two distinct navigation mechanisms.',
  tags: ['wcag2aa', 'wcag245'],
  matcher: function (node) {
    return node.nodeType === Node.ELEMENT_NODE && node.matches('body');
  },
  evaluate: function (node, options) {
    const navs = Array.from(node.querySelectorAll('nav, [role="navigation"]'));
    const search = node.querySelector('input[type="search"]');
    const siteMapLink = Array.from(node.querySelectorAll('a')).find(a =>
      a.href.includes('sitemap') || a.textContent.toLowerCase().includes('site map')
    );
    const mechanisms = navs.length + (search ? 1 : 0) + (siteMapLink ? 1 : 0);
    return mechanisms >= 2 ? [] : [{
      message: 'Less than two navigation mechanisms found.',
      nodes: [{ target: [node] }],
      failureSummary: 'Provide at least two ways to reach content (e.g., nav + search or nav + sitemap).',
      id: 'multiple-ways'
    }];
  }
};

Register the rule with axe: axe.configure({ rules: [{ id: 'multiple-ways', enabled: true }] });.

Mobile Automation

ToolWhat It ChecksConfiguration Tips
Accessibility Scanner (Android)Flags missing content descriptions, touch target size, but not navigation redundancy directly.Use it as a baseline; then add custom UIAutomator checks.
Espresso + Accessibility Test Framework (Android)Can assert that a view with a given ID or content description is displayed.Write a test that enumerates all navigation‑related views (e.g., those with contentDescription containing “menu”, “search”, “home”) and asserts count ≥ 2.
XCTest + XCUITest (iOS)Similar to Espresso; can query the app’s accessibility hierarchy.Use XCUIApplication().buttons matchingIdentifier to count nav bar items, tab bar buttons, and search fields.
Firebase Test Lab + Robo scriptCan crawl the app and collect screenshots; post‑process to detect navigation patterns.Export the crawl log and run a script that counts distinct navigation actions (e.g., clicks on bottom nav vs. drawer icon).
Detox (end‑to‑end for React Native)Can assert that a screen is reachable via multiple navigation actions.In a test, navigate to a screen via bottom tab, then reset and navigate via deep link; assert both succeed.

Example: Espresso test for Multiple Ways


@Rule
public ActivityTestRule<MainActivity> activityRule =
        new ActivityTestRule<>(MainActivity.class);

@Test
public void settingsScreenHasMultipleWays() {
    // Way 1: via bottom navigation
    onView(withId(R.id.bottom_nav_settings)).perform(click());
    onView(withText("Settings")).check(matches(isDisplayed()));

    // Reset to home
    pressBackUnconditionally();

    // Way 2: via search
    onView(withId(R.id.search_view)).perform(click());
    onView(withId(R.id.search_src_text)).perform(typeText("settings"), closeSoftKeyboard());
    onView(withText("Settings")).check(matches(isDisplayed()));
}

If either path fails, the test fails, indicating a missing alternative route.

Limitations of Automation

Fixing Violations: Code Examples (Web, Android, iOS)

Below are concrete remediation patterns for the most frequent violations identified earlier.

Web: Adding a Search Mechanism When Only a Menu Exists

Before


<header>
  <button id="hamburger" aria-label="Open menu">☰</button>
  <nav id="main-nav" style="display:none;">
    <ul>
      <li><a href="/home">Home</a></li>
      <li><a href="/products">Products</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
</header>

After


<header>
  <button id="hamburger" aria-label="Open menu">☰</button>
  <nav id="main-nav">
    <ul>
      <li><a href="/home">Home</a></li>
      <li><a href="/products">Products</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
  <!-- Search mechanism -->
  <form role="search" aria-label="Site search">
    <label for="site-search" class="visually-hidden">Search the site</label>
    <input type="search" id="site-search" placeholder="Search…" aria-controls="search-results">
    <button type="submit">Search</button>
  </form>
</header>

Web: Ensuring Breadcrumbs Appear on All Pages

Before (only on homepage)


<!-- Only on / -->
<nav aria-label="breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li aria-current="page">Home</li>
  </ol>
</nav>

After (added to a template partial)


<!-- In base layout -->
<nav aria-label="breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    {{#if currentCrumb}}
    <li><a href="{{currentCrumb.url}}">{{currentCrumb.label}}</li>
    {{/if}}
    <li aria-current="page">{{pageTitle}}</li>
  </ol>
</nav>

Android: Providing an Alternative to Edge‑Swipe Drawer

Before (drawer only opens by swipe)


<androidx.drawerlayout.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- content -->
</androidx.drawerlayout.widget.DrawerLayout>

After (add toolbar hamburger)


<androidx.drawerlayout.widget.DrawerLayout
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:navigationIcon="@drawable/ic_menu"
            app:title="MyApp" />

        <!-- main content -->
    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/drawer_menu"/>
</androidx.drawerlayout.widget.DrawerLayout>

In the Activity:


Toolbar toolbar = findViewById(R.id.toolbar);
DrawerLayout drawer = findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
        this, drawer, toolbar, R.string.navigation_drawer_open,
        R.string.navigation_drawer_close);
drawer.addDrawerListener(toggle);
toggle.syncState();

Android: Adding a Search Action to the Toolbar

Before (no search)


<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/action_refresh"
        android:title="Refresh"
        android:orderInCategory="100"
        android:showAsAction="ifRoom"/>
</menu>

After (add search)


<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/action_search"
        android:title="Search"
        android:icon="@drawable/ic_search"
        android:showAsAction="ifRoom|collapseActionView"
        android:actionViewClass="androidx.appcompat.widget.SearchView"/>
    <item
        android:id="@+id/action_refresh"
        android:title="Refresh"
        android:orderInCategory="100"
        android:showAsAction="ifRoom"/>
</menu>

In the Activity:


@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    MenuItem searchItem = menu.findItem(R.id.action_search);
    SearchView searchView = (SearchView) searchItem.getActionView();
    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            // handle search
            return false;
        }
        @Override
        public boolean onQueryTextChange(String newText) {
            // filter list
            return false;
        }
    });
    return true;
}

iOS: Ensuring Tab Bar Persists After Onboarding

Before (tab bar hidden permanently)


if !UserDefaults.standard.bool(forKey: "onboarded") {
    showOnboarding()
    tabBarController?.tabBar.isHidden = true
}

After (tab bar always visible; onboarding presented as a modal)


if !UserDefaults.standard.bool(forKey: "onboarded") {
    let onboarding = OnboardingViewController()
    onboarding.modalPresentationStyle = .fullScreen
    present(onboarding, animated: true, completion: nil)
}
// tabBar remains visible

iOS: Adding a Search Bar to Navigation Controller

Before (no search)


navigationItem.title = "Products"

After (add search controller)


let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search products"
navigationItem.searchController = searchController
definesPresentationContext = true

General Guidance for Code Fixes

  1. Use semantic elements: <nav>, <header>, <footer>, <main>, <aside> on web; Toolbar, BottomNavigationView, TabBar on mobile.
  2. Label everything: Provide aria-label or contentDescription for icons; ensure visible text labels accompany icons where possible.
  3. Maintain sufficient touch target size: 48 dp (Android) or 44 pt (iOS) minimum.
  4. Avoid relying solely on gestures: Always expose a button or control that duplicates the gesture’s function.
  5. Test with real assistive tech: TalkBack, VoiceOver, Switch Control, keyboard-only navigation, and voice commands.

Autonomous Persona‑Driven Exploration (SUSA) and How It Checks WCAG 2.4.5

SUSA (SUSATest) is an autonomous QA platform that explores an app or web property without pre‑written scripts. It builds a behavioral model of the application by exercising a range of user personas—each with distinct interaction patterns, abilities, and goals. When evaluating WCAG 2.4.5 Multiple Ways, SUSA’s persona‑driven approach adds a layer of validation that static rules and manual spot checks can miss.

How SUSA Models Personas Relevant to 2.4.5

PersonaInteraction TraitsRelevance to Multiple Ways
CuriousTaps every visible element, explores deep links, tries long‑press context menus.Discovers hidden navigation entries that may serve as a secondary path.
ImpatientPrefers shortcuts, uses search if available, abandons long flows.Will quickly use a search bar or deep link if present; failure to find it signals missing alternative.
NoviceRelies on obvious UI, avoids gestures, reads labels carefully.Will only succeed if primary navigation is clearly visible; lack of a secondary obvious path leads to repeated dead‑ends.
AdversarialAttempts to break the app, inputs unexpected values, triggers error states.May uncover states where navigation is disabled (e.g., after an error) and verify whether a fallback remains.
ElderlySlower taps, prefers larger targets, avoids complex gestures.Will favor big buttons or voice commands; if those are missing, the persona will struggle to reach screens.
AccessibilityUses screen‑reader navigation, switch control, voice commands.Will announce and attempt to activate any accessibility‑labeled navigation element; missing labels cause failures.
Power userUses keyboard shortcuts, command palettes, copy‑paste of URLs.Will try Ctrl+K or custom URL schemes; success indicates a programmatic secondary path.
HabitualRepeats learned paths, avoids exploration unless forced.If the learned path breaks (e.g., a tab removed), the persona will reveal whether another route exists.

During a test run, SUSA logs each successful navigation to a target screen and records the mechanism used (e.g., “bottom‑nav tap”, “search query”, “voice command ‘open settings’”, “deep link myapp://profile”). After the exploration phase, it computes, for each screen, the count of distinct mechanisms observed across all personas. If any screen has fewer than two distinct mechanisms, SUSA raises a WCAG 2.4.5 violation with evidence (screenshots, logs, and the persona that failed to find an alternative).

Advantages Over Pure Automation

Example CLI Invocation


# Install the agent (once)
pip install susatest-agent

# Run against a local Android emulator build
susatest-agent run \
    --apk ./app-release.apk \
    --device emulator-5554 \
    --personas curious,impatient,elderly,accessibility \
    --output ./report.json

The resulting report.json contains:


{
  "wcag245": {
    "passed": false,
    "violations": [
      {
        "screen": "Settings",
        "expectedMechanisms": 2,
        "observedMechanisms": 1,
        "mechanismsSeen": ["bottom_nav"],
        "missing": ["search"],
        "evidence": {
          "screenshot": "screenshots/settings_missing_search.png",
          "logs": [
            {"persona":"elderly","action":"swipe_up","result":"no_search_found"},
            {"persona":"accessibility","action":"voice_command_search","result":"not_recognized"}
          ]
        }
      }
    ]
  }
}

Teams can fail the build if passed is false, ensuring that any regression that removes a secondary navigation path is caught early.

Test Matrix: Combining Manual, Automated, Autonomous

ApproachStrengthsWeaknessesTypical Effort (per release)Best‑Fit Use Cases
Manual persona testingCaptures nuanced usability, discovers context‑dependent blocks, validates real‑world assistive tech.Time‑intensive, hard to scale, subjective.4‑8 hours for a medium‑size app (2 testers, 4 personas).Exploratory releases, accessibility audits, pre‑launch validation.
Automated rule‑based checks (axe, Lighthouse, Espresso/XCUITest)Fast, repeatable, catches structural missing elements, integrates in CI.Cannot assess dynamic obscurity, cognitive load, or gesture alternatives.5‑15 minutes (depends on test suite size).Every commit, nightly runs, regression guarding.
Autonomous persona‑driven exploration (SUSA)Simulates diverse behaviors, learns from prior runs, detects conditional and state‑specific issues, provides rich evidence.Requires device farm or emulator setup, slightly longer than pure unit tests.20‑40 minutes for a full app crawl (parallelizable).Pre‑release validation, periodic health checks, compliance reporting.
Hybrid (auto + SUSA + spot manual)Leverages speed of automation, depth of autonomy, and human judgment for final sign‑off.Slightly more complex pipeline setup.~1 hour total (auto 5 min + SUSA 30 min + manual 25 min).Recommended baseline for teams targeting WCAG AA conformance.

The table illustrates that no single method is sufficient alone; combining them yields the highest confidence that WCAG 2.4.5 is satisfied across all user contexts.

Checklist for Developers and QA

Use this checklist before marking a feature as “Done”. Each item should be verified for both web and mobile implementations.

✅ ItemHow to VerifyTools / Techniques
Primary navigation mechanism present (e.g., top nav, bottom nav, drawer, tab bar)Inspect DOM / view hierarchy; ensure element is focusable and announced.axe, Accessibility Scanner, manual TalkBack/VoiceOver.
At least one secondary mechanism distinct from the primary (search, site map, breadcrumbs, deep link, voice command, shortcut)Look for a second navigational element that is not a duplicate of the first.Custom axe rule, Espresso/XCUITest assertion, SUSA persona logs.
Secondary mechanism is reachable without requiring the primary (e.g., you can use search even if the drawer is hidden)Disable or hide the primary mechanism (via CSS display:none or programmatically removing the view) and attempt to reach target screens via the secondary.Manual test, automated test with state toggle.
All navigation elements have accessible names (aria-label, contentDescription, or visible label)Screen‑reader should read a purposeful description.TalkBack/VoiceOver, axe aria-allowed-attr, Accessibility Scanner.
Touch targets meet minimum size (≥48 dp Android, ≥44 pt iOS)Measure bounds or use lint rules.Android Lint, Xcode Accessibility Inspector, manual measurement with ruler app.
Navigation remains available in error and loading statesTrigger an error (e.g., invalid form submission) and verify that nav elements are still present and operable.Espresso/XCUITest test that forces error state, SUSA “adversarial” persona.
Deep links or URL schemes are documented and functionalAttempt to open the app via the link from a browser, email, or note.

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