The 28 June 2025 European Accessibility Act deadline has passed. If you build or maintain WordPress sites for clients selling to consumers in the EU (e-commerce, banking, transport, ticketing, ebooks), those clients now carry direct legal exposure if their site does not meet WCAG 2.2 AA. The audit is no longer a “nice consultancy add-on”, it is the thing the client’s legal team will ask for when the first complaint lands.
This piece is the workflow we actually run on WordPress projects: which automated tools to start with, where they stop being useful, and how to handle the parts of WCAG 2.2 that only a person with a keyboard and NVDA can verify.
Where the legal exposure actually lives
A useful frame before opening any tool: the EAA does not just cover “public sector”. It covers consumer-facing services. Private clients we have audited in the last twelve months who were in scope include a Polish ebook shop, a German furniture e-commerce, and a small EU-wide booking platform built on WooCommerce. None of them realised they were in scope until their compliance officer flagged it.
One of those clients (a WooCommerce site selling into Germany and France) received a written complaint two months after launch. The trigger was a custom checkout step where the “continue to payment” control had been built as <div onclick="..."> instead of a button. Keyboard users could not reach it, and NVDA announced nothing on focus. The remediation was small (replace with <button type="button">, restore native focus handling, announce the step transition with aria-live="polite") but the legal back-and-forth absorbed roughly a week of the agency’s time. That is the shape of the risk: not a single dramatic lawsuit, but slow, expensive correspondence that you avoid by auditing before launch and on every PR that touches DOM structure.
WCAG 2.2 itself adds three criteria that map closely to common WordPress and WooCommerce failures:
- 2.4.11 Focus Not Obscured (AA) and 2.4.13 Focus Appearance (AAA), sticky headers, cookie banners, and chat widgets routinely cover the focused element. Worth checking on every template.
- 2.5.7 Dragging Movements (AA), any drag-only interaction (slider toggles, kanban-style ordering in admin UIs, image comparison sliders on the front end) needs a single-pointer alternative such as plus/minus buttons or keyboard input.
- 2.5.8 Target Size Minimum (AA), interactive targets must be at least 24×24 CSS pixels. Most theme footers, pagination links, and social-icon strips fail this on mobile by default.
WordPress core has decent accessibility for the block editor and front-end navigation primitives. The failures we find on audits are almost always in three places: custom ACF blocks (which inherit none of Gutenberg’s ARIA wiring), theme component overrides, and third-party page builders. Plan the audit around those, not around core.
Understanding WCAG 2.2: The Foundation of Accessibility Auditing
Before diving into tools and workflows, auditors must understand the Web Content Accessibility Guidelines (WCAG) 2.2 framework. Released in October 2023, WCAG 2.2 builds upon previous versions with nine new success criteria focused on cognitive and motor accessibility.
The Four Principles of Accessibility
WCAG 2.2 organizes accessibility requirements around four foundational principles, commonly remembered by the acronym POUR:
1. Perceivable
Information and user interface components must be presentable to users in ways they can perceive. This principle covers:
- Text alternatives for non-text content (images, videos, audio)
- Captions and transcripts for multimedia
- Color contrast and text resizing capabilities
- Content that doesn’t rely solely on sensory characteristics
2. Operable
User interface components and navigation must be operable by all users. Key requirements include:
- Full keyboard accessibility
- Sufficient time limits with ability to extend
- Seizure and physical reaction prevention
- Clear navigation and wayfinding
3. Understandable
Information and the operation of user interface must be understandable:
- Readable and understandable text content
- Predictable functionality and navigation
- Input assistance and error prevention
- Consistent identification and labeling
4. Robust
Content must be robust enough to work with current and future assistive technologies:
- Valid, well-formed HTML
- Compatible with assistive technologies
- Progressive enhancement principles
- Standards-compliant markup
WCAG 2.2 Compliance Levels
WCAG 2.2 defines three levels of conformance, each building upon the previous:
| Level | Description | Typical Use Case |
|---|---|---|
| A | Minimum accessibility requirements; addresses the most critical barriers | Basic compliance, rarely sufficient alone |
| AA | Industry standard; addresses major accessibility barriers | Required by most legislation (EAA, ADA, AODA) |
| AAA | Enhanced accessibility; the highest level of conformance | Specialized applications, aspirational target |
Most legal requirements mandate WCAG 2.2 Level AA compliance. This article focuses on achieving and maintaining Level AA conformance.
New in WCAG 2.2
WCAG 2.2 introduces nine new success criteria that auditors must understand:
- 2.4.11 Focus Not Obscured (AA): Focus indicators must not be hidden by other content
- 2.4.12 Focus Not Obscured (Enhanced) (AAA): Stricter requirements for focus visibility
- 2.4.13 Focus Appearance (AAA): Specific requirements for focus indicator size and contrast
- 2.5.7 Dragging Movements (AA): Alternatives to dragging for pointer input
- 2.5.8 Target Size (Minimum) (AA): Minimum 24×24 CSS pixel target size
- 3.2.6 Consistent Help (A): Help mechanisms in consistent locations
- 3.3.7 Redundant Entry (A): Avoid requiring information already entered
- 3.3.8 Accessible Authentication (Minimum) (AA): Cognitive function tests not required for authentication
- 3.3.9 Accessible Authentication (Enhanced) (AAA): No cognitive function tests with alternatives
Automated tools: what they actually catch
Be honest with the client about this number: automated accessibility tools cover roughly 30 to 40 percent of WCAG success criteria. They are good at detecting missing alt attributes, missing form labels, insufficient contrast on static text, missing language attributes, duplicate IDs, and obviously broken ARIA. They cannot tell you whether your alt text is meaningful, whether the focus order makes sense, whether a screen reader announces a state change, or whether your custom carousel works without a mouse. The remaining 60 to 70 percent needs a human with a keyboard, a screen reader, and patience.
Run automated checks first because they are cheap and they clear the obvious failures before you waste time on manual testing. Here is the stack we actually use, in order of how often we open them.
axe DevTools (Deque)
This is the browser plugin we keep open during development. Free tier is enough for site audits; the paid version mainly adds intelligent guided testing and component-level scans. Two things make it the default:
- It tags each finding with the exact WCAG 2.2 success criterion, which lets you copy the criterion ID straight into the audit report.
- It separates “needs review” (where the tool detected a pattern that often indicates a problem) from definite violations, which keeps the false-positive rate near zero.
Workflow: scan every distinct template (homepage, archive, single post, product, checkout, contact form, login, search results) rather than every URL. Most WordPress sites have fewer than ten distinct templates even if they have thousands of pages.
Installation and Basic Usage:
- Install the axe DevTools extension from the Chrome Web Store or Firefox Add-ons
- Navigate to the page you want to test
- Open browser DevTools (F12) and select the “axe DevTools” tab
- Click “Scan all of my page” for a comprehensive analysis
- Review results categorized by severity: Critical, Serious, Moderate, Minor
Interpreting axe Results:
Critical: Blocks assistive technology users completely
Example: Missing form labels, empty buttons
Serious: Causes significant barriers for some users
Example: Insufficient color contrast, missing alt text
Moderate: Causes frustration or slower task completion
Example: Ambiguous link text, skipped heading levels
Minor: Minor inconvenience or best practice violation
Example: Redundant alternative text, redundant links
WebAIM WAVE
WAVE is useful as a second opinion and is particularly good at visualising heading structure and ARIA landmarks directly on the page. We use it specifically when a content editor has been hand-typing headings (the “h4 because it looks right” problem) and we need to show them the resulting outline. Its icon overlays make heading misuse obvious without opening DevTools.
Lighthouse Accessibility audit
Built into Chrome DevTools, free, and useful for tracking scores in Lighthouse CI alongside performance budgets. The accessibility audit underneath is a subset of axe-core, so do not treat it as authoritative, if axe DevTools is clean, Lighthouse will be cleaner. Its real value is the trend line: a Lighthouse accessibility score that drops between deployments tells you a regression was introduced and points you at the page where it landed.
Pa11y for CI integration
Pa11y is the command-line runner we wire into GitHub Actions or Bitbucket Pipelines. It runs axe-core or HTML_CodeSniffer against a list of URLs and fails the build on violations of a chosen severity. The reason to bother: accessibility regressions, like performance regressions, are cheapest to fix in the pull request that introduced them. By the time QA finds them in staging, three other changes have piled on top and the diff is harder to read.
Tenon API for fleet checks
If you maintain dozens of WordPress sites under one agency umbrella, Tenon’s API is the most efficient way to run nightly cross-site scans and aggregate the results. It is overkill for a single-site audit and worth setting up the moment you cross five or six client sites where any regression is your problem to triage.
WordPress-specific helpers
Plugins do not make a site accessible, but a few of them remove the most common WordPress-shaped failures and free you up to spend audit time on the harder problems. Three notes before installing anything: alt-text discipline in the media library is worth more than any plugin (audit it during onboarding), Gutenberg core blocks ship with reasonable ARIA but custom ACF blocks almost never do (audit those individually), and breadcrumb components from theme frameworks frequently lack aria-label="Breadcrumb" on the surrounding <nav>.
WP Accessibility
WP Accessibility addresses common WordPress accessibility issues without requiring code changes:
Features:
- Removes target attributes from links (prevents unexpected new windows)
- Adds skip links for keyboard navigation
- Forces alt attributes on images (with fallback handling)
- Adds labels to standard WordPress forms
- Provides toolbar with font size and contrast controls
Configuration Best Practices:
// Recommended WP Accessibility settings for audit preparation
// Settings → WP Accessibility
// Enable: Add skip links to your theme
// Enable: Add landmark roles to HTML5 structural elements
// Enable: Force alt attributes on images
// Enable: Add post titles to "read more" links
// Disable: Remove title attributes (can conflict with some themes)
Accessibility Checker by Equalize Digital
This plugin provides real-time accessibility checking within the WordPress admin:
- Editor Integration: Scans content as you create it
- Frontend Scanning: Checks published pages from visitor perspective
- Detailed Reports: Shows exact code causing issues
- Bulk Scanning: Audit entire sites automatically
Pro Version Features:
- Full-site automated scanning
- PDF report generation
- Issue prioritization
- Remediation guidance
One Click Accessibility
A lightweight plugin for quick accessibility improvements:
- Skip to content link
- Outline focus for keyboard navigation
- Font resizing controls
- Grayscale mode for testing
- Light and high contrast modes
CI/CD Integration for Continuous Accessibility
Automated accessibility testing should be part of your continuous integration pipeline to prevent regressions.
axe-core with GitHub Actions
# .github/workflows/accessibility.yml
name: Accessibility Tests
on: [push, pull_request]
jobs:
accessibility:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install @axe-core/cli http-server
- name: Build site
run: npm run build
- name: Start server
run: npx http-server ./dist -p 8080 &
- name: Run axe accessibility tests
run: |
npx axe http://localhost:8080 \
--tags wcag2a,wcag2aa,wcag22aa \
--exit
Pa11y for Automated Scanning
Pa11y is a command-line tool that can be integrated into any CI/CD pipeline:
# Install Pa11y
npm install -g pa11y
# Run accessibility test on a single page
pa11y https://example.com --standard WCAG2AA
# Run with actions (click elements, wait for content)
pally https://example.com --actions "click element #menu-toggle"
# Generate JSON report for CI integration
pa11y https://example.com --reporter json --standard WCAG2AA > accessibility-report.json
Lighthouse CI for Performance and Accessibility
Google’s Lighthouse CI can track accessibility scores over time:
// lighthouserc.json
{
"ci": {
"collect": {
"url": [
"http://localhost:8080/",
"http://localhost:8080/about/",
"http://localhost:8080/contact/"
],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:accessibility": ["error", {"minScore": 0.9}]
}
}
}
}
Automated vs. Manual Testing Comparison
| Aspect | Automated Testing | Manual Testing |
|---|---|---|
| Coverage | ~30% of WCAG criteria | ~100% with proper methodology |
| Speed | Minutes for entire site | Hours for comprehensive audit |
| Cost | Low (tools are often free) | Higher (requires expertise) |
| Consistency | Highly reproducible | Depends on tester skill |
| False Positives | Minimal with quality tools | None |
| False Negatives | Significant (misses ~70%) | Minimal with thorough testing |
| Best For | Baseline compliance, regression prevention | Comprehensive audits, legal compliance |
| Frequency | Every deployment | Quarterly or major releases |
Manual testing: keyboard, screen reader, contrast
This is where most of the audit time goes, and where the criteria that actually decide a complaint live: keyboard operability, screen-reader announcements, focus management, and contrast. Three tools in three sessions, in this order.
Keyboard-only navigation
Unplug the mouse. Tab through every distinct template from the top of the page to the footer, then back up with Shift+Tab. Activate every interactive element with Enter or Space. The questions you are answering:
- Can you reach every interactive element, including those revealed by hover (mega menus, tooltips, “view more” reveals)?
- Is the focus indicator always visible? On dark backgrounds, the default browser outline often vanishes. WCAG 2.2’s Focus Appearance criterion (2.4.13) wants at least a 2 CSS pixel thick indicator with a 3:1 contrast ratio against adjacent colours.
- Does focus get obscured by sticky headers, cookie banners, or chat widgets? This is criterion 2.4.11 and it is the single most common WCAG 2.2 failure on sites built before 2024.
- Is there any interaction that requires a drag? Image comparison sliders, range inputs styled as drag handles, and admin-side reorder controls all need a single-pointer alternative under 2.5.7.
- Are interactive targets at least 24×24 CSS pixels (criterion 2.5.8)? Pagination links, social icons in footers, and inline “x” close buttons fail this most often.
The Keyboard Testing Protocol
Step 1: Basic Navigation
- Disconnect your mouse or disable your trackpad
- Press Tab to move through interactive elements (links, buttons, form fields)
- Press Shift+Tab to move backward
- Use Enter to activate links and buttons
- Use Space to activate buttons and checkboxes
- Use Arrow keys for radio buttons, selects, and custom widgets
Step 2: Focus Visibility Testing
Verify that focus indicators are clearly visible:
/* Ensure focus indicators are visible */
/* WCAG 2.2 requires focus indicators to be at least 2px thick */
/* Good: Clear focus indicator */
a:focus,
button:focus,
input:focus {
outline: 3px solid #0056b3;
outline-offset: 2px;
}
/* Bad: No focus indicator or insufficient contrast */
a:focus {
outline: none; /* Removes focus indicator entirely */
}
Step 3: Logical Tab Order
Tab order should follow the visual reading order (typically left-to-right, top-to-bottom). Check for:
- Tab traps (can’t Tab away from an element)
- Skipped elements that should be focusable
- Illogical jumps in focus order
- Hidden elements receiving focus
Step 4: Interactive Element Testing
Test all interactive components:
| Element | Keyboard Operation | Expected Behavior |
|---|---|---|
| Links | Tab, Enter | Navigate to target |
| Buttons | Tab, Enter, Space | Activate action |
| Checkboxes | Tab, Space | Toggle selection |
| Radio buttons | Tab, Arrow keys | Change selection within group |
| Select dropdowns | Tab, Arrow keys, Enter/Space | Open and select options |
| Modal dialogs | Tab (trapped), Escape | Focus trapped, close on Escape |
| Accordion | Tab, Enter/Space, Arrow keys | Expand/collapse panels |
| Tabs | Tab, Arrow keys, Enter | Switch between tab panels |
Common Keyboard Issues in WordPress
Issue: Missing focus styles in themes
/* Fix: Add visible focus styles to your theme */
/* Add to style.css or customizer additional CSS */
*:focus {
outline: 2px solid #007cba;
outline-offset: 2px;
}
/* Enhanced focus for high contrast */
@media (prefers-contrast: high) {
*:focus {
outline: 3px solid currentColor;
outline-offset: 3px;
}
}
Issue: Custom dropdowns not keyboard accessible
<!-- Bad: Custom dropdown without keyboard support -->
<div class="dropdown">
<span class="dropdown-trigger">Menu</span>
<div class="dropdown-content">
<a href="#">Item 1</a>
<a href="#">Item 2</a>
</div>
</div>
<!-- Good: Accessible dropdown with ARIA -->
<div class="dropdown">
<button
class="dropdown-trigger"
aria-expanded="false"
aria-haspopup="true"
id="menu-button"
>
Menu
</button>
<div
class="dropdown-content"
role="menu"
aria-labelledby="menu-button"
hidden
>
<a href="#" role="menuitem">Item 1</a>
<a href="#" role="menuitem">Item 2</a>
</div>
</div>
Screen reader testing
The pragmatic answer to “which screen reader” is: NVDA on Windows for the bulk of the audit, JAWS for any client who will hand the report to an enterprise compliance team, and VoiceOver on macOS or iOS when the site has significant Apple-platform traffic. NVDA is free and is the de-facto standard in WebAIM’s own surveys of screen-reader users, so it is what your real users will most often be running. JAWS still matters for sign-off because larger corporate accessibility teams treat it as the reference. VoiceOver matters because mobile Safari is unavoidable.
What you are listening for during the audit: that the screen reader announces a meaningful name for every interactive element, that headings come through in a sensible outline (H1, then H2s, no skipped levels), that form fields read out the label and any help text, that error messages are announced when they appear (not just rendered visually), and that dynamic content changes use aria-live regions where appropriate. If you find a <div onclick> masquerading as a button, NVDA will go silent on it, that is your tell.
Screen Reader Options
| Screen Reader | Platform | Cost | Best For |
|---|---|---|---|
| NVDA | Windows | Free | Primary Windows testing, most popular |
| JAWS | Windows | Paid ($95/year) | Enterprise compliance testing |
| VoiceOver | macOS/iOS | Built-in | Apple ecosystem testing |
| TalkBack | Android | Built-in | Mobile Android testing |
| Narrator | Windows | Built-in | Quick Windows testing |
NVDA Testing Workflow
NVDA (NonVisual Desktop Access) is the most widely used screen reader and essential for Windows accessibility testing.
Installation:
- Download from nvaccess.org
- Run the installer (portable version available)
- Launch with Ctrl+Alt+N
Essential NVDA Commands for Testing:
Basic Navigation:
- Tab: Move to next focusable element
- Shift+Tab: Move to previous focusable element
- H: Next heading
- 1-6: Next heading level 1-6
- L: Next list
- I: Next list item
- T: Next table
- F: Next form field
- G: Next graphic
- D: Next landmark
Reading Commands:
- Insert+Down Arrow: Start reading continuously
- Ctrl: Stop reading
- Insert+Up Arrow: Read current line
- Insert+Tab: Read current focus
Mode Switching:
- Insert+Space: Toggle between browse and focus modes
What to Listen For:
- Meaningful announcements: Does the screen reader convey the purpose of each element?
- Context: Are form fields properly labeled?
- Structure: Are headings announced with their levels?
- State changes: Are dynamic updates announced (live regions)?
- Navigation: Can you navigate by headings, landmarks, and lists?
VoiceOver Testing on macOS
VoiceOver is built into macOS and essential for testing Apple devices.
Enabling VoiceOver:
- Press Cmd+F5 (or triple-click Touch ID on newer Macs)
- Or: System Settings → Accessibility → VoiceOver
Essential VoiceOver Commands:
Basic Navigation:
- Cmd+F5: Toggle VoiceOver
- Ctrl+Option+Right/Left Arrow: Navigate to next/previous item
- Ctrl+Option+Shift+Down Arrow: Enter/interact with element
- Ctrl+Option+Shift+Up Arrow: Exit element
Web Navigation:
- Ctrl+Option+Cmd+H: Next heading
- Ctrl+Option+Cmd+L: Next link
- Ctrl+Option+Cmd+J: Next form control
- Ctrl+Option+Cmd+T: Next table
Reading:
- Ctrl+Option+A: Start reading
- Ctrl: Stop reading
Mobile Screen Reader Testing
Mobile accessibility is critical as mobile traffic continues to grow.
iOS VoiceOver Testing:
- Settings → Accessibility → VoiceOver → Toggle On
- Use single finger swipe right to navigate
- Double-tap to activate
- Three-finger swipe to scroll
Android TalkBack Testing:
- Settings → Accessibility → TalkBack → Toggle On
- Use single finger swipe right to navigate
- Double-tap to activate
- Two-finger swipe to scroll
Contrast verification
We measure contrast with the WebAIM Contrast Checker (webaim.org/resources/contrastchecker/) for spot checks and Chrome DevTools’ colour picker for in-context checks against actual rendered backgrounds (which matters when text sits over an image or a gradient). Two contrast failures hide in almost every WordPress theme: light-grey “muted” body copy that fails 4.5:1 against white, and placeholder text in form inputs that the designer styled to look subtle and now reads at 2.5:1. WCAG 2.2 also brings UI components and focus indicators into the contrast scope at 3:1, which is where icon-only buttons and outlined-style focus rings tend to fail.
Contrast Requirements
| Content Type | Level A | Level AA | Level AAA |
|---|---|---|---|
| Normal text (<18pt or <14pt bold) | 3:1 | 4.5:1 | 7:1 |
| Large text (≥18pt or ≥14pt bold) | 3:1 | 3:1 | 4.5:1 |
| UI Components & Graphics | 3:1 | 3:1 | 3:1 |
| Focus indicators (WCAG 2.2) | 3:1 | 3:1 | 4.5:1 |
Testing Tools
Browser DevTools:
Modern browsers include contrast checking in DevTools:
- Right-click element → Inspect
- In Styles panel, click color swatch
- Check contrast ratio displayed
- Look for green checkmark (passes) or warning icon (fails)
WebAIM Contrast Checker:
WebAIM’s online tool provides detailed analysis:
- Visit webaim.org/resources/contrastchecker/
- Enter foreground and background colors
- Review pass/fail status for each level
- Use the lightness slider to find passing alternatives
Sim Daltonism:
Test how your site appears to users with color vision deficiencies:
- macOS: Available on the App Store
- Windows: ColorVeil or similar tools
- Online: Colorblindly browser extension
Common Contrast Issues in WordPress
Issue: Light gray text on white backgrounds
/* Bad: Insufficient contrast */
.text-muted {
color: #999999; /* 2.8:1 contrast - FAILS WCAG AA */
}
/* Good: Sufficient contrast */
.text-muted {
}
Issue: Placeholder text contrast
/* Bad: Default placeholder often fails */
::placeholder {
}
/* Good: Darker placeholder */
::placeholder {
}
Focus Management Testing
WCAG 2.2 places increased emphasis on focus visibility and management.
Focus Not Obscured (2.4.11)
When a UI component receives keyboard focus, the component must not be entirely hidden by author-created content.
Testing Procedure:
- Navigate through the page using Tab
- Verify focused elements are fully visible
- Check for sticky headers/footers that might obscure focus
- Test modals and overlays for focus trapping
Common Issue: Sticky Headers Hiding Focus
/* Problem: Sticky header covers focused elements */
.header-sticky {
position: fixed;
top: 0;
height: 80px;
}
/* Solution: Scroll padding for fixed headers */
html {
scroll-padding-top: 100px; /* Header height + buffer */
}
/* Or use :target offset for anchor links */
:target {
scroll-margin-top: 100px;
}
Focus Appearance (2.4.13 - AAA)
While AAA level, understanding focus appearance helps create better experiences:
/* Enhanced focus indicator meeting AAA guidelines */
:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
border-radius: 2px;
}
/* Minimum area: 48×48 CSS pixels */
.focus-enhanced:focus-visible {
box-shadow: 0 0 0 2px white, 0 0 0 4px currentColor;
}
Common WordPress Accessibility Issues and Fixes
WordPress sites share common accessibility patterns. Understanding these accelerates auditing and remediation.
Issue 1: Missing or Inadequate Alternative Text
The Problem:
Images without alt text or with meaningless filenames as alt text.
Detection:
- Automated tools flag missing alt attributes
- Screen readers announce “image” or filename
The Fix:
<!-- Bad: Missing alt text -->
<img src="photo.jpg">
<!-- Bad: Meaningless alt text -->
<img src="photo.jpg" alt="photo">
<!-- Good: Descriptive alt text -->
<img src="wordpress-dashboard.jpg" alt="WordPress admin dashboard showing the block editor with a paragraph block selected">
<!-- Good: Decorative image with empty alt -->
<img src="decorative-border.png" alt="">
WordPress Implementation:
// Enforce alt text in WordPress media library
function enforce_alt_text($response, $attachment, $meta) {
if (empty($response['alt'])) {
$response['alt'] = ''; // Mark as decorative by default
}
return $response;
}
add_filter('wp_prepare_attachment_for_js', 'enforce_alt_text', 10, 3);
// Add alt text reminder in admin
function alt_text_admin_notice() {
$screen = get_current_screen();
if ($screen && $screen->id === 'attachment') {
echo '<div class="notice notice-warning">
<p><strong>Accessibility Reminder:</strong> Please add descriptive alt text or mark as decorative.</p>
</div>';
}
}
add_action('admin_notices', 'alt_text_admin_notice');
Issue 2: Improper Heading Structure
The Problem:
Skipped heading levels, using headings for styling, or missing H1.
Detection:
- axe flags skipped heading levels
- WAVE shows heading outline
- Screen reader users navigate by headings
The Fix:
<!-- Bad: Skipped levels and styling misuse -->
<h1>Page Title</h1>
<h4>Section Heading</h4> <!-- Skipped h2, h3 -->
<h2 style="font-size: 14px;">Small Text</h2> <!-- Using h2 for styling -->
<!-- Good: Logical heading hierarchy -->
<h1>Page Title</h1>
<h2>Main Section</h2>
<h3>Subsection</h3>
<h3>Another Subsection</h3>
<h2>Another Main Section</h2>
WordPress Block Editor Fix:
// Restrict heading levels in block editor
function restrict_heading_levels($settings) {
$settings['allowedBlockTypes'] = array(
'core/paragraph',
'core/heading',
'core/list',
// ... other allowed blocks
);
return $settings;
}
// Or use CSS to visually indicate heading level
function admin_heading_styles() {
echo '<style>
h1.wp-block-heading { border-left: 4px solid #007cba; padding-left: 8px; }
h2.wp-block-heading { border-left: 4px solid #00a0d2; padding-left: 8px; }
h3.wp-block-heading { border-left: 4px solid #46b450; padding-left: 8px; }
</style>';
}
add_action('admin_head', 'admin_heading_styles');
Issue 3: Form Labels and Error Messages
The Problem:
Unlabeled form fields, missing error associations, or unclear error messages.
Detection:
- axe flags form fields without labels
- Screen readers don’t announce field purpose
- Error messages aren’t programmatically associated
The Fix:
<!-- Bad: No label association -->
<input type="email" placeholder="Enter your email">
<!-- Bad: Placeholder as label -->
<input type="email" placeholder="Email Address">
<!-- Good: Explicit label with for attribute -->
<label for="email">Email Address <span aria-label="required">*</span></label>
<input type="email" id="email" name="email" required aria-required="true">
<!-- Good: Error message programmatically associated -->
<label for="email">Email Address *</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid="true"
aria-describedby="email-error"
>
<span id="email-error" class="error" role="alert">
Please enter a valid email address (e.g., name@example.com)
</span>
WordPress Contact Form 7 Enhancement:
// Add ARIA attributes to Contact Form 7
add_filter('wpcf7_form_elements', function($content) {
// Add aria-required to required fields
$content = preg_replace(
'/<input[^>]*class="[^"]*wpcf7-validates-as-required[^"]*"[^>]*>/',
'$0 aria-required="true"',
$content
);
return $content;
});
Issue 4: Keyboard-Inaccessible Custom Components
The Problem:
Custom dropdowns, tabs, and modals that only work with mouse.
The Fix:
// Accessible Tab Component
class AccessibleTabs {
constructor(container) {
this.container = container;
this.tabs = Array.from(container.querySelectorAll('[role="tab"]'));
this.panels = Array.from(container.querySelectorAll('[role="tabpanel"]'));
this.tabs.forEach((tab, index) => {
tab.addEventListener('click', () => this.selectTab(index));
tab.addEventListener('keydown', (e) => this.handleKeydown(e, index));
});
}
handleKeydown(event, index) {
let newIndex;
switch(event.key) {
case 'ArrowRight':
newIndex = (index + 1) % this.tabs.length;
break;
case 'ArrowLeft':
newIndex = (index - 1 + this.tabs.length) % this.tabs.length;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = this.tabs.length - 1;
break;
default:
return;
}
event.preventDefault();
this.tabs[newIndex].focus();
this.selectTab(newIndex);
}
selectTab(index) {
this.tabs.forEach((tab, i) => {
const isSelected = i === index;
tab.setAttribute('aria-selected', isSelected);
tab.setAttribute('tabindex', isSelected ? '0' : '-1');
this.panels[i].hidden = !isSelected;
});
}
}
// Initialize
document.querySelectorAll('[role="tablist"]').forEach(tabs => {
new AccessibleTabs(tabs);
});
<!-- Accessible Tab Markup -->
<div class="tabs">
<div role="tablist" aria-label="Product Information">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
tabindex="0"
>
Description
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1"
>
Specifications
</button>
</div>
<div
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
>
Product description content...
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
hidden
>
Technical specifications...
</div>
</div>
Issue 5: Missing Skip Links
The Problem:
Users navigating by keyboard must tab through all navigation items to reach main content.
The Fix:
<!-- Skip Link Implementation -->
<body>
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<nav><!-- Navigation items --></nav>
<main id="main-content" tabindex="-1">
<!-- Main content -->
</main>
</body>
/* Skip Link Styling */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
WordPress Implementation:
// Add skip link in header.php (before any other content)
function add_skip_link() {
echo '<a href="#main-content" class="skip-link">' .
esc_html__('Skip to main content', 'textdomain') .
'</a>';
}
add_action('wp_body_open', 'add_skip_link');
// Ensure main content area has ID
echo '<main id="main-content" tabindex="-1">';
Issue 6: Inaccessible Modal Dialogs
The Problem:
Modals that don’t trap focus, close on Escape, or return focus to trigger.
The Fix:
// Accessible Modal Component
class AccessibleModal {
constructor(trigger, modal) {
this.trigger = trigger;
this.modal = modal;
this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
this.firstFocusable = null;
this.lastFocusable = null;
this.previousFocus = null;
trigger.addEventListener('click', () => this.open());
modal.querySelector('.modal-close').addEventListener('click', () => this.close());
modal.addEventListener('keydown', (e) => this.handleKeydown(e));
}
open() {
this.previousFocus = document.activeElement;
this.modal.hidden = false;
this.modal.setAttribute('aria-modal', 'true');
const focusable = this.modal.querySelectorAll(this.focusableElements);
this.firstFocusable = focusable[0];
this.lastFocusable = focusable[focusable.length - 1];
this.firstFocusable.focus();
document.body.style.overflow = 'hidden';
}
close() {
this.modal.hidden = true;
this.modal.removeAttribute('aria-modal');
document.body.style.overflow = '';
this.previousFocus.focus();
}
handleKeydown(event) {
if (event.key === 'Escape') {
this.close();
}
if (event.key === 'Tab') {
if (event.shiftKey && document.activeElement === this.firstFocusable) {
event.preventDefault();
this.lastFocusable.focus();
} else if (!event.shiftKey && document.activeElement === this.lastFocusable) {
event.preventDefault();
this.firstFocusable.focus();
}
}
}
}
<!-- Accessible Modal Markup -->
<button aria-haspopup="dialog" aria-controls="modal-1" id="modal-trigger">
Open Modal
</button>
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
id="modal-1"
hidden
>
<h2 id="modal-title">Modal Title</h2>
<p>Modal content...</p>
<button class="modal-close" aria-label="Close modal">
<span aria-hidden="true">×</span>
</button>
</div>
Creating an Accessibility Audit Report
A well-structured audit report transforms findings into actionable remediation plans.
Report Structure
1. Executive Summary
- Overall compliance status (WCAG 2.2 Level AA)
- Critical issues count
- Risk assessment
- Recommended priority order
2. Methodology
- Tools used (axe, WAVE, screen readers)
- Pages tested (representative sample)
- Testing dates and environment
3. Detailed Findings
Organize by WCAG principle (Perceivable, Operable, Understandable, Robust) or by severity.
Finding Template:
### Issue: [Brief Description]
**WCAG Criterion:** [e.g., 1.1.1 Non-text Content (Level A)]
**Severity:** [Critical | Serious | Moderate | Minor]
**Location:** [URL and specific element]
**Description:**
Detailed explanation of the issue and its impact on users.
**Current Code:**
```html
<!-- Problematic code -->
Recommended Fix:
<!-- Corrected code -->
Testing Verification:
- [Step to verify fix]
- [Expected result]
**4. Remediation Roadmap**
| Priority | Issue | Estimated Effort | Owner | Target Date |
|----------|-------|------------------|-------|-------------|
| P0 | Missing form labels | 2 hours | Developer | Week 1 |
| P0 | Keyboard traps | 4 hours | Developer | Week 1 |
| P1 | Color contrast | 3 hours | Designer | Week 2 |
| P2 | Heading structure | 2 hours | Content | Week 3 |
### Automated Report Generation
**Using axe-core for Reports:**
```javascript
// generate-accessibility-report.js
const { axe } = require('@axe-core/webdriverjs');
const { Builder } = require('selenium-webdriver');
const fs = require('fs');
async function auditPage(url) {
const driver = await new Builder().forBrowser('chrome').build();
try {
await driver.get(url);
const results = await axe(driver).analyze();
const report = {
url,
timestamp: new Date().toISOString(),
violations: results.violations.map(v => ({
id: v.id,
impact: v.impact,
description: v.description,
help: v.help,
helpUrl: v.helpUrl,
nodes: v.nodes.length
})),
passes: results.passes.length,
incomplete: results.incomplete.length,
inapplicable: results.inapplicable.length
};
fs.writeFileSync(
`audit-${Date.now()}.json`,
JSON.stringify(report, null, 2)
);
return report;
} finally {
await driver.quit();
}
}
// Audit multiple pages
const pages = [
'https://example.com/',
'https://example.com/about/',
'https://example.com/contact/'
];
pages.forEach(url => auditPage(url));
Report Templates
Spreadsheet Template Columns:
- Issue ID
- WCAG Criterion
- Level (A/AA/AAA)
- Page/URL
- Element Location
- Issue Description
- User Impact
- Recommended Fix
- Effort Estimate
- Priority
- Status
- Notes
Remediation Workflow
Transforming audit findings into an accessible website requires systematic remediation.
Phase 1: Critical Issues (Week 1)
Focus on barriers that completely block users:
- Missing form labels - Prevents form completion
- Keyboard traps - Locks users in elements
- Missing alt text on informative images - Blocks content understanding
- Empty buttons/links - Prevents interaction
Daily Standup Questions:
- Which critical issues were resolved today?
- Are there any blockers preventing fixes?
- Have fixes been tested with keyboard and screen reader?
Phase 2: Serious Issues (Week 2-3)
Address significant barriers that cause frustration:
- Color contrast failures
- Skipped heading levels
- Missing skip links
- Improper ARIA usage
Phase 3: Moderate Issues (Week 4)
Polish the experience:
- Redundant links
- Ambiguous link text
- Missing language attributes
- PDF accessibility
Testing Verification Protocol
For each fix, verify with:
- Automated re-test: Run axe on the specific page
- Keyboard verification: Navigate to and interact with the element
- Screen reader check: Confirm proper announcement
- Visual inspection: Check focus indicators and contrast
Regression Prevention
Pre-commit Hooks:
// package.json
{
"husky": {
"hooks": {
"pre-commit": "npm run test:accessibility"
}
},
"scripts": {
"test:accessibility": "axe http://localhost:3000 --exit"
}
}
Visual Regression Testing:
Include accessibility-specific visual tests:
// accessibility-visual.test.js
describe('Focus indicators', () => {
it('should show visible focus on buttons', async () => {
await page.goto('http://localhost:3000');
await page.keyboard.press('Tab');
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot();
});
});
Related Resources
For more comprehensive WordPress optimization and development guidance, explore these related articles:
- Semantic SEO for WordPress: The Complete 2026 Guide - Learn how accessibility improvements enhance your SEO strategy
- WordPress Contact Forms: The Ultimate Guide for 2026 - Ensure your forms meet WCAG standards
- How to Update URLs in the WordPress Database When Moving to a New Domain - Maintain accessibility during site migrations
LLM-Friendly Structured Data
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Practical Accessibility Auditing: Tools & Workflow for WordPress",
"description": "A comprehensive guide to auditing WordPress sites for WCAG 2.2 compliance using automated tools and manual testing techniques.",
"author": {
"@type": "Organization",
"name": "WPPoland",
"url": "https://wppoland.com"
},
"publisher": {
"@type": "Organization",
"name": "WPPoland",
"logo": {
"@type": "ImageObject",
"url": "https://wppoland.com/logo.png"
}
},
"datePublished": "2026-01-29",
"dateModified": "2026-01-29",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://wppoland.com/blog/practical-accessibility-auditing-workflow"
},
"about": {
"@type": "Thing",
"name": "Web Accessibility Auditing",
"sameAs": "https://www.w3.org/WAI/standards-guidelines/wcag/"
},
"teaches": [
"WCAG 2.2 compliance testing",
"Automated accessibility testing with axe and WAVE",
"Manual testing with screen readers",
"Keyboard navigation testing",
"WordPress accessibility remediation"
],
"proficiencyLevel": "Intermediate",
"dependencies": "WordPress, Modern web browser, Screen reader software"
}
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Conduct a WCAG 2.2 Accessibility Audit on WordPress",
"description": "Step-by-step workflow for auditing WordPress websites for accessibility compliance",
"totalTime": "PT4H",
"supply": [
"Web browser (Chrome, Firefox, or Edge)",
"axe DevTools browser extension",
"WAVE browser extension",
"Screen reader (NVDA, JAWS, or VoiceOver)"
],
"tool": [
{
"@type": "HowToTool",
"name": "axe DevTools"
},
{
"@type": "HowToTool",
"name": "WAVE Evaluation Tool"
},
{
"@type": "HowToTool",
"name": "NVDA Screen Reader"
}
],
"step": [
{
"@type": "HowToStep",
"position": 1,
"name": "Run Automated Tests",
"text": "Install axe DevTools and WAVE browser extensions. Scan all key pages including homepage, contact page, and content templates. Document all violations by severity level.",
"url": "https://wppoland.com/blog/practical-accessibility-auditing-workflow#automated-testing-tools"
},
{
"@type": "HowToStep",
"position": 2,
"name": "Test Keyboard Navigation",
"text": "Disconnect your mouse and navigate the entire site using only Tab, Shift+Tab, Enter, Space, and Arrow keys. Verify all interactive elements are reachable and operable.",
"url": "https://wppoland.com/blog/practical-accessibility-auditing-workflow#keyboard-navigation-testing"
},
{
"@type": "HowToStep",
"position": 3,
"name": "Conduct Screen Reader Testing",
"text": "Test with NVDA (Windows) or VoiceOver (macOS). Navigate by headings, landmarks, and form fields. Verify all content is properly announced and navigation is logical.",
"url": "https://wppoland.com/blog/practical-accessibility-auditing-workflow#screen-reader-testing"
},
{
"@type": "HowToStep",
"position": 4,
"name": "Verify Color Contrast",
"text": "Use browser DevTools or WebAIM Contrast Checker to verify all text meets WCAG 2.2 contrast requirements (4.5:1 for normal text, 3:1 for large text).",
"url": "https://wppoland.com/blog/practical-accessibility-auditing-workflow#color-contrast-verification"
},
{
"@type": "HowToStep",
"position": 5,
"name": "Document and Prioritize Findings",
"text": "Create a comprehensive report organizing issues by WCAG principle and severity. Develop a remediation roadmap with timelines and responsible parties.",
"url": "https://wppoland.com/blog/practical-accessibility-auditing-workflow#creating-an-accessibility-audit-report"
},
{
"@type": "HowToStep",
"position": 6,
"name": "Remediate and Verify Fixes",
"text": "Fix critical issues first, followed by serious and moderate issues. Verify each fix with automated tools, keyboard testing, and screen reader confirmation.",
"url": "https://wppoland.com/blog/practical-accessibility-auditing-workflow#remediation-workflow"
}
]
}
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is the difference between WCAG 2.1 and WCAG 2.2?",
"acceptedAnswer": {
"@type": "Answer",
"text": "WCAG 2.2 builds upon WCAG 2.1 with nine new success criteria focusing on cognitive and motor accessibility. Key additions include Focus Not Obscured (2.4.11), Dragging Movements (2.5.7), and Accessible Authentication (3.3.8). WCAG 2.2 also deprecates success criterion 4.1.1 Parsing."
}
},
{
"@type": "Question",
"name": "How often should I conduct accessibility audits?",
"acceptedAnswer": {
"@type": "Answer",
"text": "For active websites, conduct automated scans with every deployment and comprehensive manual audits quarterly or with major releases. After significant updates - theme changes, plugin additions, or content migrations - perform targeted audits of affected areas."
}
},
{
"@type": "Question",
"name": "Can I achieve WCAG compliance using only automated tools?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. Automated tools catch approximately 30% of WCAG criteria, primarily those that can be programmatically verified. Manual testing with keyboard navigation and screen readers is essential for evaluating cognitive accessibility, meaningful alternative text, and complex interaction patterns."
}
},
{
"@type": "Question",
"name": "What screen reader should I use for testing?",
"acceptedAnswer": {
"@type": "Answer",
"text": "NVDA is the most widely used screen reader and is free, making it the best starting point for Windows testing. For comprehensive compliance testing, also test with JAWS (the enterprise standard) and VoiceOver (built into macOS and iOS)."
}
},
{
"@type": "Question",
"name": "Are there legal risks for non-compliant WordPress websites?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. In the European Union, the European Accessibility Act mandates WCAG 2.2 Level AA compliance for public sector websites and essential services, with penalties varying by member state (up to €100,000 in some jurisdictions). In the United States, ADA Title III lawsuits have increased significantly."
}
}
]
}

