# Accessibility Audit for Next.js Apps: Automating WCAG Compliance

> How to audit Next.js apps for accessibility issues using axe-core and Playwright — with the workflow that catches 57% of WCAG violations automatically.

URL: https://www.matthewswong.com/en/blog/accessibility-audit-nextjs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2025-06-10
- Category: Web Dev

Only 4.1% of websites meet WCAG 2.2 Level AA standards. The WebAIM Million 2025 report found 95.9% of websites fail basic accessibility requirements, averaging 51 errors per page. With 4,605 ADA lawsuits filed against websites in 2024 and the European Accessibility Act enforceable from June 2025, accessibility is now a legal and business risk, not just a best practice. Axe-core, Deque's open-source accessibility engine (downloaded 4 billion times), can automatically detect 57% of WCAG violations — the other 43% require manual review. I've integrated axe-core into the CI pipeline for matthewswong.com and it catches real issues before every deploy.

## What Automated Tools Can and Can't Catch

Automated accessibility tools excel at structural issues: missing alt text on images, form inputs without labels, insufficient color contrast, missing ARIA roles, heading hierarchy violations, keyboard focus order problems, and missing landmark regions. These are the mechanical rules of WCAG. What they can't catch: whether alt text is meaningful (they can check it exists, not whether it describes the image accurately), whether a custom widget is actually keyboard-navigable by a screen reader user, or whether the reading order makes semantic sense. Automated testing is the starting point, not the complete solution.

### axe-core: The Automation Layer

Axe-core runs accessibility checks in a browser context and returns structured results: violations (definite issues), incomplete (checks that require manual review), passes, and inapplicable (rules that don't apply to the current page). Each violation includes the HTML element causing the issue, the WCAG success criterion violated, an impact rating (critical/serious/moderate/minor), and remediation guidance. The @axe-core/playwright package integrates axe with Playwright tests — check entire pages or individual components.

### The Keyboard Navigation Test

The simplest manual accessibility test: unplug your mouse and try to use your own application with only the keyboard. Tab to navigate between interactive elements. Enter/Space to activate buttons. Arrow keys for menus and sliders. Escape to close modals. If you can't reach a feature with keyboard alone, keyboard-only users (and screen reader users, who rely on keyboard navigation) can't access it either. In my Next.js projects, I run this test on every new interactive component before shipping. Playwright can automate keyboard navigation testing with `page.keyboard.press('Tab')` sequences.

> From my experience auditing matthewswong.com for accessibility: the most common violations I found were 1) icon buttons without accessible names (an X button to close a modal with no aria-label or sr-only text), 2) color contrast failures on text over gradient backgrounds, and 3) missing focus indicators on custom-styled interactive elements. All three are easy to fix once you know they exist. Add aria-label to icon buttons, check contrast with a tool like WebAIM's contrast checker, and ensure your CSS focus styles are visible (never just `outline: none` without a replacement).

## Integrating axe-core into Playwright Tests

The @axe-core/playwright package adds a `checkA11y()` method that runs axe in the current browser context and fails the test if violations are found. Run it after navigating to each page in your E2E test suite. This catches accessibility regressions introduced by any code change — a new component, a CSS change that reduces contrast, or a missing label on a new form field. Configure it to only fail on critical and serious violations initially, then tighten to moderate as you resolve issues.

### Color Contrast and Tailwind

Tailwind's default color palette is accessible — Tailwind 3+ includes contrast ratio information in the docs for each color combination. The risk is custom colors added to tailwind.config.ts: a brand color that looks fine visually but fails the 4.5:1 contrast ratio for normal text (WCAG AA). Check all custom colors with WebAIM's contrast checker or the axe DevTools browser extension. For text on colored backgrounds, follow the rule: normal text needs 4.5:1, large text (18px+ or 14px bold) needs 3:1. When in doubt, use Tailwind's official palette rather than custom colors.

> **Aria Attributes Are Easy to Misuse**

> Adding ARIA attributes to fix accessibility issues without understanding them is dangerous — incorrect ARIA can make a page less accessible than no ARIA at all. The ARIA authoring practices rule: 'No ARIA is better than bad ARIA.' Common mistakes: adding role='button' to a div instead of using a real button element (you then must implement keyboard focus, Enter key, and Space key handling manually), using aria-hidden on visible content, and nesting interactive elements inside elements with aria-label. Before adding ARIA to fix an audit finding, read the ARIA Authoring Practices Guide for the pattern you're implementing.

## Screen Reader Testing

Automated tools and keyboard testing cover a lot, but screen reader testing reveals how your content flows when visual layout is stripped away. Test with NVDA (free, Windows), JAWS (the most common enterprise screen reader, Windows), or VoiceOver (built into macOS and iOS). The key things to verify: headings create a logical outline (use the screen reader's heading navigation to see your h1-h6 structure), links have descriptive text ('click here' is useless to screen reader users), form fields announce their labels when focused, and error messages are announced after form submission.

## Building Accessibility Into the Component Library

The highest-leverage approach: fix accessibility at the component level so every page that uses the component is accessible by default. If your Button component always has a visible focus state and your Input always has a paired label, pages are accessible by default without per-page audit. I use Radix UI primitives in my Next.js projects — they're headless, unstyled, and fully keyboard-accessible and screen-reader-accessible by specification. Styling Radix components with Tailwind gives you accessible components without writing ARIA logic from scratch.

## The CI Workflow I Use

My accessibility CI workflow: Playwright tests run axe-core on all critical pages (home, login, dashboard, key user flows) on every PR. Violations are reported as GitHub Actions annotations with the element HTML and remediation guidance. Critical violations (missing labels, keyboard traps) fail the build. Serious violations post a warning but don't block merge — giving the team a week to fix before the threshold tightens. Monthly, I run a full axe-cli scan across all pages and review the incomplete list for manual remediation. The incremental approach has moved matthewswong.com from 12 violations to 0 critical violations in three months.

## Sources & Further Reading

- Deque — axe-core Open Source Accessibility Engine
- Playwright — Accessibility Testing
- AllAccessible — Website Accessibility Audit WCAG 2.2 Guide

## Source links

- https://www.deque.com/axe/axe-core/
- https://playwright.dev/docs/accessibility-testing
- https://www.allaccessible.org/blog/website-accessibility-audit-guide-wcag-template

## FAQ

### What percentage of WCAG violations can axe-core catch automatically, and what does manual review still cover?

Axe-core automatically detects 57% of WCAG violations, according to the WebAIM Million 2025 report context cited in the post. The remaining 43% require manual review — for example, whether alt text is actually meaningful, whether a custom widget is truly keyboard-navigable by a screen reader user, and whether reading order makes semantic sense.

### What were the most common accessibility violations found when auditing matthewswong.com?

The three most common violations were: icon buttons without accessible names (e.g. a close-modal X button with no aria-label or sr-only text), color contrast failures on text over gradient backgrounds, and missing focus indicators on custom-styled interactive elements. All three are straightforward to fix once identified — add aria-label to icon buttons, verify contrast ratios, and ensure CSS focus styles are visible.

### How does the CI workflow integrate axe-core with Playwright?

The @axe-core/playwright package adds a checkA11y() method that runs axe in the current browser context and fails the test if violations are found. In the CI pipeline, Playwright tests run axe-core on all critical pages on every PR, with critical violations failing the build outright and serious violations posting a warning without blocking the merge. A full axe-cli scan across all pages is also run monthly.

### Why is incorrect ARIA usage potentially worse than using no ARIA at all?

The ARIA Authoring Practices Guide principle cited in the post is 'No ARIA is better than bad ARIA.' A common mistake is adding role='button' to a div instead of using a real button element — you then have to manually implement keyboard focus, Enter key, and Space key handling. Incorrect ARIA can suppress native browser semantics and make a page harder to use for screen reader users than if no ARIA were present.

### How does using Radix UI primitives help maintain accessibility across a Next.js component library?

Radix UI primitives are headless and unstyled, but fully keyboard-accessible and screen-reader-accessible by specification. By fixing accessibility at the component level — using Radix primitives styled with Tailwind — every page that uses a given component inherits correct accessibility behavior by default, without requiring per-page audits or custom ARIA logic.

---

- HTML version: https://www.matthewswong.com/en/blog/accessibility-audit-nextjs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/accessibility-audit-nextjs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
