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
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:
- Screen‑reader users who may find a linear list of links easier to traverse than a hidden hamburger menu.
- Users with motor impairments who struggle with small touch targets; a search bar with voice input can reduce the need for precise gestures.
- Cognitive‑disability users who benefit from redundancy; if they forget where a feature lives, a second route reduces frustration.
- Power users who prefer keyboard shortcuts or command‑palette style navigation.
- Elderly users who may not be familiar with gesture‑based navigation and rely on visible menus.
When only a single path exists, a failure can manifest as:
- Inability to reach a settings screen because the only entry point is a hidden gesture that TalkBack does not announce.
- Missing a promotional offer because the only way to view it is via a swipe‑up carousel that lacks a visible button.
- Abandoning a checkout flow because the “Edit cart” link is only reachable through a long‑press context menu that is not exposed to switch‑control users.
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 Type | Description | Example Code | Why It Fails 2.4.5 |
|---|---|---|---|
| Hidden navigation | Main 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 map | Content‑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 JavaScript | A 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 pages | Only 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
| Violation | Description | Kotlin/XML Snippet | Failure Reason |
|---|---|---|---|
| Navigation drawer only reachable by swipe from edge | No 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 modal | The 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 UI | A 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 navigation | A 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
| Violation | Description | Swift/Snippet | Failure Reason |
|---|---|---|---|
| Tab bar hidden behind a modal onboarding flow | After 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 list | No 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 action | On 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 exit | An interactive pop gesture is disabled, forcing users to rely solely on a programmatically placed back button that may be off‑screen. | navigationController?.interactivePopGestureRecognizer?.isEnabled = false | Users 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
- List all reachable screens: Use the app or site as a typical user would, noting every distinct screen (e.g., Home, Profile, Settings, Product Detail, Checkout Confirmation). For web, a sitemap generator (e.g., Screaming Frog) can help enumerate URLs. For mobile, tools like
adb shell dumpsys activityor Xcode’s debug view hierarchy can list visible view controllers. - Identify navigation mechanisms: For each screen, record the mechanisms that lead to it. Common mechanisms include:
- Global navigation (top nav, bottom nav, drawer, tab bar)
- Local controls (buttons, links, list items)
- Search (site‑wide or scoped)
- Breadcrumbs
- Site map or help page
- Deep links / URL schemes
- Voice commands or shortcuts
- Context menus (long‑press, right‑click)
- Keyboard shortcuts (e.g.,
Ctrl+Kfor command palette)
2. Verification
For each screen, attempt to reach it using at least two of the identified mechanisms while simulating different user profiles:
| Profile | Technique | What to Observe |
|---|---|---|
| Screen‑reader user | Navigate using TalkBack/VoiceOver; listen for announcements of navigation elements. | Ensure each mechanism is announced and activatable. |
| Motor‑impairment user | Use switch control, head tracking, or voice input; avoid precise gestures. | Verify that at least one mechanism does not require fine‑grained touch. |
| Keyboard‑only user | Tab through interactive elements; use Enter/Space to activate. | Confirm that navigation is reachable without mouse. |
| Cognitive‑load user | Follow 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 user | Try 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:
| Screen | Mechanism 1 | Mechanism 2 | Pass/Fail | Notes |
|---|---|---|---|---|
| Home | Top nav | Logo link | Pass | Both announced by TalkBack. |
| Settings | Bottom nav | Deep link (myapp://settings) | Fail | Deep link works but no UI entry; switch control cannot activate bottom nav due to obscured hit‑test. |
| Product Detail | Breadcrumb | Search | Pass | Breadcrumb visible; search returns product. |
| Checkout Confirmation | Order history link | Email link | Fail | Email 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
| Tool | What It Checks | How 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. |
| Lighthouse | Accessibility audit includes “navigation” checks. | In CI, run lighthouse --only-categories=accessibility --preset=desktop and assert score ≥ 90 for the “navigation” sub‑score. |
| pa11y | Scriptable 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-axe | End‑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
| Tool | What It Checks | Configuration 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 script | Can 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
- Automation can confirm the presence of navigation elements but cannot guarantee they are usable (e.g., a nav item may be present but hidden behind a modal that only appears after a gesture).
- It cannot assess cognitive load or the clarity of labels.
- Therefore, automated checks should be complemented with manual persona‑based testing and, where possible, autonomous exploration.
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>
- The hamburger button is now focusable and the menu is displayed by default (or toggled via ARIA‑expanded).
- A search form provides a second mechanism.
- Use
visually-hiddenclass to hide the label visually but keep it accessible to screen readers.
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>
- The breadcrumb is rendered on every layout, providing a second navigational cue alongside the global menu.
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();
- The toolbar icon provides a second, tap‑based mechanism that is announced by TalkBack as “Open navigation drawer”.
- Ensure the icon has a content description (provided by MaterialToolbar) and sufficient touch target size (≥48 dp).
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;
}
- The search view is announced as “Search button, double tap to edit” and provides a keyboard‑friendly mechanism.
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
- Users who skip onboarding still see the tab bar, giving them a second way to navigate (tab bar + any deep link or search).
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
- The search bar is announced by VoiceOver as “Search field, double tap to edit”.
- It provides an alternative to scrolling through a long list.
General Guidance for Code Fixes
- Use semantic elements:
<nav>,<header>,<footer>,<main>,<aside>on web;Toolbar,BottomNavigationView,TabBaron mobile. - Label everything: Provide
aria-labelorcontentDescriptionfor icons; ensure visible text labels accompany icons where possible. - Maintain sufficient touch target size: 48 dp (Android) or 44 pt (iOS) minimum.
- Avoid relying solely on gestures: Always expose a button or control that duplicates the gesture’s function.
- 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
| Persona | Interaction Traits | Relevance to Multiple Ways |
|---|---|---|
| Curious | Taps every visible element, explores deep links, tries long‑press context menus. | Discovers hidden navigation entries that may serve as a secondary path. |
| Impatient | Prefers 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. |
| Novice | Relies 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. |
| Adversarial | Attempts 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. |
| Elderly | Slower 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. |
| Accessibility | Uses screen‑reader navigation, switch control, voice commands. | Will announce and attempt to activate any accessibility‑labeled navigation element; missing labels cause failures. |
| Power user | Uses keyboard shortcuts, command palettes, copy‑paste of URLs. | Will try Ctrl+K or custom URL schemes; success indicates a programmatic secondary path. |
| Habitual | Repeats 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
- Context‑sensitive: SUSA notices when a navigation item is present but obscured by a modal that only appears after a specific user action (e.g., a promo overlay). The “Impatient” persona may dismiss the overlay quickly, while the “Elderly” persona may get stuck, revealing a conditional block.
- Dynamic state coverage: By simulating personas that interact with forms, error dialogs, and permission prompts, SUSA can verify that navigation remains available in error states—a scenario often missed by static checks.
- Learning across sessions: The platform remembers which screens were reached via which mechanisms. In subsequent runs, it prioritizes unexplored combinations, gradually building a comprehensive map of redundant paths.
- Integration with CI: The CLI
susatest-agentcan be invoked as a step in a pipeline, uploading an APK or providing a URL, and returning a JSON report that includes awcag245section with pass/fail status and detailed logs.
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
| Approach | Strengths | Weaknesses | Typical Effort (per release) | Best‑Fit Use Cases |
|---|---|---|---|---|
| Manual persona testing | Captures 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.
| ✅ Item | How to Verify | Tools / 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 states | Trigger 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 functional | Attempt 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