Conducting Manual Accessibility Audits Systematically
AI generated
A11Y
WCAG
Accessibility · WCAG 2.2 · Manual Audits · Testing
Conducting Manual Accessibility Audits Systematically
from keyboard pass to a prioritized findings list

Automated scanners like axe-core catch only a fraction of the real barriers in an application. This guide shows how a structured manual audit with keyboard pass, screen reader pass, zoom and contrast pass, and reduced motion pass runs systematically, how findings are prioritized by real user impact instead of raw violation count, and how automated and manual results can be combined into one report that developers can actually act on.

17 min read Keyboard Pass · Screen Reader Pass · Prioritization WCAG 2.2 · axe-core · NVDA · VoiceOver

1. Why manual audits need to complement automated scanners

Automated testing tools like axe-core, Lighthouse or WAVE reliably check machine-detectable patterns: missing alt attributes, invalid ARIA combinations, duplicate IDs or insufficient color contrast on static text. Independent studies keep showing the same picture, though: automated scanners realistically cover only about 30 to 40 percent of the WCAG success criteria relevant in a real application. The rest requires human judgment because it depends on context.

Whether a focus order is logical, whether a screen reader announcement actually sounds understandable, or whether alt text meaningfully describes an image, no rule set can decide that alone. A manual accessibility audit fills exactly this gap: it checks actual operability with keyboard, screen reader and altered display conditions, instead of only matching the DOM tree against a rule list. Both testing approaches are not mutually exclusive but complementary: automation covers breadth quickly and cheaply, the manual audit covers depth and real usage scenarios.

2. The structured audit workflow at a glance

A repeatable audit workflow needs a fixed structure, otherwise the result depends on chance, on which page the tester happens to open. A sequence of four consecutive passes has proven effective: keyboard-only pass, screen reader pass, zoom and contrast pass, and reduced motion pass. Each pass simulates a different real usage situation and thereby uncovers different barriers. Before the first pass, a fixed page list is defined: critical user flows like login, cart, checkout and forms take priority over edge pages.

It is important to run each pass in isolation instead of testing keyboard, screen reader and zoom at the same time. Mixed testing leads to incomplete notes because symptoms overlap. Each pass ends with its own raw list of observations, which is only evaluated and prioritized afterward together with the automated scan results. A complete run through all four passes for a typical checkout flow takes, in practice, between four and eight hours, depending on the number of templates and form steps.

3. Keyboard-only pass: tab order, focus and operability

In the keyboard-only pass, the mouse is physically set aside or disabled and the entire page is operated exclusively with Tab, Shift+Tab, Enter, Space and the arrow keys. What is checked: does focus reach every interactive element in a logical order? Is the focus indicator clearly visible at all times? Can every action possible with the mouse also be triggered by keyboard? Modals, dropdown menus and custom widgets are especially critical: when a dialog opens, does focus land inside it automatically, and does it stay trapped there until the dialog is closed?

A frequent finding is the so-called keyboard trap, where focus gets stuck inside a component and Tab no longer leads out of it. Just as common: tabindex values greater than zero, which override the natural DOM order and lead to an unpredictable jump order for keyboard users. The test rarely takes longer than five minutes per page, but it often uncovers the most severe findings of the entire audit, because a blocked checkout step completely prevents a transaction.


<!-- Skip link must be the first focusable element in the DOM -->
<a href="#main-content" class="skip-link">Skip to main content</a>

<style>
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  background: #000;
  color: #fff;
  padding: 0.75rem 1rem;
}
/* Skip link becomes visible only when it receives keyboard focus */
.skip-link:focus {
  left: 0;
}
</style>

<!-- WRONG: positive tabindex overrides natural DOM order -->
<button tabindex="3">Save</button>
<button tabindex="1">Cancel</button>

<!-- RIGHT: rely on DOM order, tabindex="0" only for custom widgets -->
<div role="button" tabindex="0" aria-pressed="false" id="toggle-filter">
  Toggle filter
</div>

<!-- Modal must trap focus while open and restore it on close -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <p id="dialog-title" class="dialog-heading">Confirm deletion</p>
  <button id="dialog-confirm">Delete</button>
  <button id="dialog-cancel">Cancel</button>
</div>

4. Screen reader pass: NVDA, VoiceOver and JAWS compared

The screen reader pass tests whether content and interactions are read out in an understandable way when the screen is not available as a visual reference. In practice, a combination of at least NVDA with Firefox on Windows is recommended, because this pairing is free and has the widest market share, along with VoiceOver with Safari on macOS or iOS for Apple users. JAWS remains relevant in enterprise environments but is paid and behaves differently in details than NVDA, which is why critical flows are ideally tested with both.

Navigation is tested via headings (the H key in NVDA), landmarks, form fields and links, each time without looking at the screen. Key checkpoints: does every interactive control have a meaningful accessible name? Are dynamic status changes such as cart updates announced automatically? Is the state of checkboxes, toggles and accordions communicated correctly? An icon-only button without aria-label is often read by NVDA merely as "button" with no further context, which makes it effectively unusable.


<!-- WRONG: icon-only button has no accessible name -->
<button class="icon-btn">
  <svg aria-hidden="true"><!-- cart icon --></svg>
</button>

<!-- RIGHT: accessible name via aria-label, including dynamic state -->
<button class="icon-btn" aria-label="Open cart (3 items)">
  <svg aria-hidden="true"><!-- cart icon --></svg>
</button>

<!-- Live region for asynchronous status updates -->
<!-- Must already exist in the DOM before the update happens -->
<div aria-live="polite" aria-atomic="true" class="sr-only" id="cart-status"></div>

<!-- Clearing before writing ensures identical consecutive messages -->
<!-- are still announced by the screen reader -->
<!-- element.textContent = ''; requestAnimationFrame(() => element.textContent = text); -->

5. Zoom and contrast pass: 200% zoom and WCAG contrast values

The zoom and contrast pass checks WCAG criterion 1.4.4 (resize text): the page is zoomed to 200 percent in the browser without content being cut off, overlapping, or forcing horizontal scrolling. In addition, the reflow test per 1.4.10 checks the layout at 400 percent zoom in a viewport reduced to 320 pixels wide, which corresponds to a typical smartphone view under strong magnification. Both tests reliably surface rigid pixel widths, fixed container heights and clipped dropdown menus.

In parallel, color contrast is measured: normal text needs at least 4.5:1 against the background, large text (24px or 19px bold and above) at least 3:1, and non-text UI components such as focus rings or icon buttons also at least 3:1 per criterion 1.4.11. Tools like the Colour Contrast Analyser or the contrast check in Chrome DevTools measure actually rendered colors, including gradients and background images, which automated scanners often fail to capture correctly. An extra look at dark mode is worthwhile too, because inverted color schemes frequently introduce their own contrast problems.

6. Reduced motion pass: animations and motion triggers

The reduced motion pass checks whether a page respects the system setting prefers-reduced-motion, which users with vestibular disorders or migraines enable to avoid nausea or dizziness triggered by motion. Testing is done by enabling the setting in the operating system's accessibility options (macOS: Accessibility, Display, Reduce Motion; Windows: Ease of Access, Display) and then re-running through carousels, parallax effects, autoplaying videos and animated GIFs.

A frequent finding: CSS animations correctly respect the media query, but a JavaScript-driven carousel does not check matchMedia('(prefers-reduced-motion: reduce)') and keeps running unchanged despite the setting being enabled. Autoplaying background videos without pause controls are another recurring pattern that violates criterion 2.2.2 (pause, stop, hide). The pass takes only a few minutes per page but uncovers issues that, for affected user groups, make the difference between usable and a genuine health risk.


/* Respect user preference: disable non-essential animation */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .hero-carousel {
    animation: none;
  }
}

/* Default: animation only runs when explicitly allowed by the user */
@media (prefers-reduced-motion: no-preference) {
  .fade-in {
    animation: fadeIn 400ms ease-out;
  }
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

7. Prioritizing by real user impact instead of violation count

A report with 200 automatically detected violations looks alarming but is often misleading: 150 of them can be identical, minor contrast deviations in footer links, while a single finding, a keyboard trap in checkout, completely prevents purchase completion for every keyboard user. The raw number of rule violations is therefore not a suitable prioritization criterion. What matters is the actual user impact: does the finding fully block a core task, does it significantly hinder it, or is it merely a comfort issue with no task blockage?

A practical prioritization matrix combines three factors: severity of impact (blocking, hindering, cosmetic), reach (global navigation and checkout weigh more heavily than a rarely visited subpage), and WCAG conformance level (level A violations before AA before AAA). A finding that violates WCAG A and occurs on every checkout step gets the rating "critical", regardless of how many automated rules it triggers in the scanner report. This prioritization should be calibrated together with the development team, so capacity is invested where the effect on users is greatest.

8. Documenting findings developers can actually act on

A finding like "contrast too low" without a page reference, screenshot or concrete color value is barely usable for a development team and leads to follow-up questions that delay the entire fix. A usable audit write-up contains at minimum: exact reproduction steps, the affected WCAG success criterion including conformance level, expected versus actual behavior, the exact code location or component, a concrete fix recommendation, and the affected user group. Screenshots or short screen recordings are especially helpful for focus and screen reader findings, because they are otherwise hard to follow.

A structured, machine-readable format for every finding additionally simplifies later evaluation, tracking in the ticket system, and reuse in regression tests. Instead of free text in a spreadsheet, a unified schema is recommended that enforces the same fields for every finding, regardless of which pass uncovered it. This makes it possible to compare findings across different audits and spot recurring issues across releases.


{
  "id": "A11Y-0042",
  "wcag_criterion": "2.4.7 Focus Visible",
  "wcag_level": "AA",
  "pass_type": "keyboard",
  "severity": "critical",
  "user_impact": "Keyboard users lose orientation in checkout because the focus ring was removed via CSS.",
  "affected_pages": ["/checkout", "/checkout/payment"],
  "steps_to_reproduce": [
    "Operate the page exclusively with the Tab key, do not use a mouse",
    "Tab to the 'Place order' button",
    "Visually check the focus indicator"
  ],
  "expected": "Clearly visible focus ring around the active button",
  "actual": "No visible difference between focused and unfocused state",
  "recommended_fix": "Remove outline: none, define :focus-visible with a 3px contrast ring instead",
  "code_location": "web/css/source/_buttons.css:112",
  "source": "manual",
  "status": "open"
}

9. Combining manual and automated findings into one report

Separate reports for automated scanner results and manual audit findings almost always lead to a team consistently working through only the automated results, because they integrate more easily into the CI pipeline and ticket system. The more effective approach is a combined report: axe-core results from the CI run are normalized to the same schema as the manual findings, with a shared severity model, a source tag (automated or manual), and a single prioritized list as the basis for sprint planning.

Deduplication matters: an automatically detected contrast error and a manually documented finding on the same component should be merged into one entry, not land twice in the backlog. The table below shows which check areas cannot be covered by automation alone and why the corresponding manual test step remains necessary.


// Merge axe-core CI results with manually documented findings into one unified report
import axeResults from './reports/axe-ci-run.json' assert { type: 'json' };
import manualFindings from './reports/manual-audit.json' assert { type: 'json' };

function normalizeAxeViolation(violation) {
  return {
    id: `AXE-${violation.id}`,
    wcag_criterion: violation.tags.find(t => t.startsWith('wcag2')) ?? 'unmapped',
    severity: mapImpactToSeverity(violation.impact),
    source: 'automated',
    affected_pages: violation.nodes.map(n => n.target.join(' ')),
    status: 'open',
  };
}

function mapImpactToSeverity(impact) {
  // axe-core impact levels do not map 1:1 to real user severity
  const map = { critical: 'critical', serious: 'high', moderate: 'medium', minor: 'low' };
  return map[impact] ?? 'medium';
}

const unifiedReport = [
  ...axeResults.violations.map(normalizeAxeViolation),
  ...manualFindings,
].sort((a, b) => severityRank(a.severity) - severityRank(b.severity));

function severityRank(severity) {
  return { critical: 0, high: 1, medium: 2, low: 3 }[severity] ?? 4;
}

console.log(`Unified report: ${unifiedReport.length} findings`);
Check area Automated coverage only Manual test step Why it matters
Color contrast Only static DOM color values Zoom and contrast pass with real rendering Hover, focus and image states are otherwise missed
Tab order Checks only DOM order Keyboard-only pass with visual check Visual and DOM order can diverge
Screen reader announcements Only ARIA syntax errors Screen reader pass with real listening Comprehensibility can only be judged by humans
Visible focus Detects missing focus ring unreliably Keyboard-only pass with visual review Visibility depends on actual rendering
Motion content No motion rules in axe-core Reduced motion pass with system setting Vestibular risks are only visible in real operation

Mironsoft

Manual accessibility audits and WCAG-compliant implementation for Magento and Hyvä stores

Need a professional manual accessibility audit?

We combine keyboard, screen reader, zoom and reduced motion passes with automated scans, prioritize findings by real user impact, and deliver a report your development team can act on directly.

Manual audit

Four structured passes across critical user flows

Prioritized report

Findings sorted by user impact, not violation count

Implementation support

Concrete code fixes and re-testing after release

10. Summary

Manual accessibility audits close the gap that automated scanners systematically leave open. Four consecutive passes, keyboard-only, screen reader, zoom and contrast, and reduced motion, each uncover their own barriers that only real usage reveals. What matters most for practical value is prioritizing by real user impact instead of the raw number of rule violations, so development resources are invested where users are actually blocked.

Structured, schema-based documentation makes findings directly actionable for development teams instead of generating follow-up questions and delays. The biggest lever is merging automated and manual results into a single, deduplicated report instead of maintaining two separate backlogs, of which only one ends up being consistently worked through in the long run.

Conducting Manual Accessibility Audits Systematically: Key Takeaways

Four structured passes

Keyboard-only, screen reader, zoom/contrast and reduced motion together cover what scanners miss.

Impact-based prioritization

Task-blocking findings before cosmetic violations, regardless of violation count.

Actionable documentation

Reproduction steps, WCAG criterion, code location and concrete fix in every finding.

Combined report

Merge automated and manual results into one deduplicated, prioritized list.

11. FAQ: Conducting Manual Accessibility Audits Systematically

1What is a manual accessibility audit and why is automated testing not enough?
Automated scanners cover only about 30 to 40 percent of WCAG criteria, because many require human judgment. The manual audit checks actual operability.
2Which four passes should a manual audit include?
Keyboard-only, screen reader, zoom/contrast and reduced motion, each run in isolation for complete, unambiguous observations.
3How do I run a keyboard-only pass correctly?
Disable the mouse, operate only with Tab, Shift+Tab, Enter and Space. Check focus order, visibility and keyboard traps.
4Which screen readers should I use for an audit?
At least NVDA with Firefox and VoiceOver with Safari. JAWS in addition for enterprise environments, since it behaves differently in details.
5How do I test zoom and contrast behavior correctly?
Test 200 percent zoom and reflow at 400 percent in a 320 pixel viewport. Measure contrast against actually rendered colors, at least 4.5:1.
6What is a reduced motion pass and how do I test it?
Checks whether prefers-reduced-motion is respected. Enable the setting in the operating system and re-test carousels and autoplay videos.
7How do I prioritize findings correctly instead of going by violation count?
By real user impact based on severity, reach and WCAG conformance level, not raw violation count.
8How do I document a finding so developers can act on it directly?
With reproduction steps, WCAG criterion, expected versus actual behavior, code location and a concrete fix recommendation.
9How do I combine automated tool results with manual findings in one report?
Normalize both sources to the same schema, merge duplicate findings, and maintain them as one prioritized list.
10How often should a manual accessibility audit be repeated?
Critical flows with every major release, complemented by a full annual audit. CI scans catch regressions in between.