Automating Accessibility Testing in the Vue CI Pipeline with axe-core
AI generated
{ }
Vue · Accessibility · CI
axe-core in the CI Pipeline
Automated accessibility testing for Vue applications

axe-core checks a rendered page against a fixed catalog of automatable accessibility rules, such as color contrast, missing form labels, or incorrectly nested headings, and integrates through @axe-core/playwright in end-to-end tests as well as through vitest-axe in component tests. It's important to know the limits of automated checking, though, since tab order logic and content clarity fall outside what it can cover.

14 min read axe-core Accessibility CI

1. What axe-core can check automatically

axe-core is a rule engine that analyzes a page's rendered DOM and checks it against an extensive catalog of accessibility rules, mostly derived from the WCAG guidelines. Reliably automatable violations include insufficient color contrast between text and background, missing label elements or aria-label attributes on form fields, images without an alt attribute, and structural issues like duplicate IDs or incorrectly nested heading levels.

These rules can be automated reliably because they can be derived purely from the DOM's structure and computed style properties, without a human having to judge whether a piece of text makes sense in context or whether an interaction is actually intuitive to use. axe-core's own documentation estimates it covers roughly 30 to 50 percent of all known accessibility issues, meaning a passing axe-core check is a necessary but not sufficient condition for genuine accessibility.

2. Setup with vitest-axe for component tests

For isolated component tests, vitest-axe integrates axe-core directly into Vitest and extends the test assertions with a toHaveNoViolations() matcher. After rendering a component with Vue Test Utils, its HTML output gets passed to axe-core, which runs the check against the full rule catalog and returns a list of found violations that the matcher then checks against an empty expectation.

This approach fits especially well for individual, reusable components like buttons, form fields, or modals, since violations get caught at the component level before they propagate to every page that uses that component. A missing ARIA attribute on a central button component becomes visible right in the component test, instead of only surfacing later in a more expensive end-to-end test on a full page.


import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { axe, toHaveNoViolations } from 'vitest-axe'
import FormField from '@/components/FormField.vue'

expect.extend(toHaveNoViolations)

describe('FormField Accessibility', () => {
  it('has no automatically detectable accessibility violations', async () => {
    const wrapper = mount(FormField, {
      props: { label: 'Email', modelValue: '', type: 'email' },
    })

    const results = await axe(wrapper.element)
    expect(results).toHaveNoViolations()
  })
})

3. Setup with @axe-core/playwright for E2E tests

For checks at the level of a complete, browser-rendered page, @axe-core/playwright runs the axe-core engine inside a real browser context after Playwright has loaded the page. This variant additionally catches problems that only emerge from the actual interplay of multiple components on a page, such as duplicate IDs that don't stand out in isolated component tests individually but become a genuine violation once several instances of the same component land on the same page.

The typical flow has Playwright navigate to the target page and, if needed, establish a particular application state, such as an open modal or a filled-out form, before AxeBuilder runs the analysis against the current DOM state. That makes it possible to check not just the initial page state but also dynamically generated UI states like open dropdowns or displayed error messages, which would be harder to simulate realistically in a purely component-test environment.

4. What axe-core CANNOT detect automatically

The most important limit of automated checking concerns logical tab order and, more generally, actual keyboard operability. axe-core can check whether an interactive element is focusable at all, but not whether the order in which elements are reached via the tab key makes sense to a user. A form where the tab order visually jumps top to bottom but is reversed in the DOM stays invisible to axe-core, yet it confuses any user navigating exclusively with a keyboard.

Equally unautomatable are content-quality questions, such as whether an alt text is meaningful even though it's technically present but written uninformatively, for example alt="image1" instead of an actual description. axe-core only checks whether the attribute exists and isn't empty, not whether its content is actually helpful to a screen reader user. Whether an error message gets announced to a screen reader user at the right moment and with the right urgency also often requires manual testing with a real screen reader.

5. Building a CI gate that blocks critical violations

A sensible CI gate distinguishes between violation severities, which axe-core itself classifies into critical, serious, moderate, and minor. A pragmatic approach fails the build on critical and serious violations, while moderate and minor violations initially just appear as a warning in the CI log without blocking the merge. That prevents a team from being immediately blocked, right at rollout, by a large number of existing but low-severity legacy issues.

In practice, this is implemented by filtering axe-core's violations output and checking specifically for the critical categories before the test runner exits with an error status. Over time, the threshold can be tightened gradually, for instance by first applying strict rules only to new components while existing pages are temporarily exempted through a documented exception list until they get cleaned up.

6. Documenting known exceptions properly

Not every violation axe-core reports is immediately fixable, especially when it lives inside a third-party component whose markup isn't directly under your control. For these cases, axe-core lets you selectively disable specific rules or exclude specific selectors from the check, for example through AxeBuilder's disableRules() method or the rules option in the axe configuration.

It's important to comment each exception directly in the code and record why it exists, rather than disabling it silently. An uncommented exception looks at first glance like a successfully passed check, but actually hides a real problem that should eventually become visible again once the underlying cause can be fixed, for example after an update to the affected third-party library.

7. Runtime and performance in the CI pipeline

axe-core checks add extra runtime to the CI pipeline, especially with @axe-core/playwright, where every checked page needs a real browser context along with full rendering. For large applications with many pages, it pays off not to fully check every single page on every commit, but instead to regularly test a representative selection of critical pages and run a full check of all pages less often, for example nightly or before a release.

vitest-axe checks at the component level are considerably cheaper, since no real browser needs to launch and only the isolated component tree gets checked. These component tests can run comfortably on every commit and form the fast first line of defense, while the more expensive end-to-end checks with Playwright get used more selectively and less often.

8. Why manual testing remains necessary

A CI gate with axe-core doesn't replace manual accessibility testing; it reduces the number of issues that would otherwise even need to be found manually in the first place. Especially newly built, complex interactive components like multi-step forms, date pickers, or drag-and-drop interfaces should additionally be tested with actual keyboard navigation and, where possible, a real screen reader like NVDA or VoiceOver, since exactly these are where the problems automated checking can't cover tend to show up most often.

A sensible team workflow combines automated axe-core checking as an early, fast filter in CI with a manual testing checklist for critical user flows, run before every major release. That combination ensures obvious, technical violations never make it to production in the first place, while the more demanding, context-dependent aspects of accessibility stay covered through deliberate human review.

9. Configuring the rule set through WCAG tags

Both @axe-core/playwright and vitest-axe let you narrow the checked rule set through tags, such as wcag2a, wcag2aa, or wcag21aa, instead of automatically running the full, very broad default catalog. A team that wants to focus initially only on WCAG level AA criteria can restrict the check specifically to that subset through withTags(['wcag2a', 'wcag2aa']) on AxeBuilder, which makes the result list more manageable and eases rollout in an existing project.

Beyond this tag-based scoping, axe-core can also be extended with custom, project-specific rules, for example to check internal design-system conventions that go beyond the standard WCAG criteria. This extensibility turns axe-core into more than a pure WCAG checking tool; it becomes a general platform for automatable accessibility quality rules that can adapt to a project's actual requirements instead of rigidly following a single predefined rule set.

Automatically detectable (axe-core) NOT automatically detectable
Insufficient color contrast Logical tab order
Missing labels/ARIA attributes Content clarity of alt text
Missing alt attribute on images Actual screen reader announcement quality
Duplicate IDs Intuitiveness of an interaction
Incorrectly nested headings Context-dependent usability of complex widgets

Mironsoft

Vue architecture, Composition API, and Nuxt performance

Vue applications that don't get more complicated with every feature?

We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.

Architecture Review

Checking composables, state management, and component structure for maintainability.

Performance Audit

Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.

Nuxt Integration

Building robust, type-safe SSR/SSG setup and API integration.

10. Summary

axe-core in CI: key takeaways at a glance

Coverage

axe-core covers an estimated 30 to 50 percent of accessibility issues automatically

Tools

vitest-axe for component tests, @axe-core/playwright for full pages

CI gate

critical and serious violations block the build, minor ones only warn

Limit

tab order logic and content quality still require manual review

11. FAQ: axe-core in CI: key takeaways at a glance

1What's the difference between vitest-axe and @axe-core/playwright?
vitest-axe checks isolated components inside a Vitest run without a real browser, while @axe-core/playwright checks complete pages rendered in a real browser through Playwright.
2What percentage of accessibility issues does axe-core cover?
According to its developers, roughly 30 to 50 percent of known issues, since many aspects require context-dependent human judgment.
3Can axe-core check tab order?
No, axe-core can check whether elements are focusable at all, but not whether the actual order of focus makes logical sense to a user.
4Should every axe-core violation block the CI build?
A pragmatic approach blocks only on critical and serious violations, while moderate and minor ones initially just appear as warnings, so rollout doesn't get blocked.
5How do I handle violations inside third-party components?
Through targeted exceptions using disableRules() or the rules option, with every exception commented and justified in the code.
6Does an axe-core CI gate replace manual screen reader testing?
No, it only reduces the number of obvious, technical violations. Complex interactive components should still be tested manually with real screen readers.
7Does axe-core noticeably slow down the CI pipeline?
vitest-axe component tests are cheap and can run on every commit. @axe-core/playwright tests are more expensive and fit better for a representative selection of critical pages.
8Can axe-core check whether an alt text is meaningful?
No, it only checks whether the alt attribute exists and isn't empty, not whether its content is actually informative to a screen reader user.
9What severity levels does axe-core distinguish?
critical, serious, moderate, and minor, which allows a nuanced decision about which categories should block the build.
10Is axe-core worth it for small projects too?
Yes, because even basic checks like missing labels or contrast issues are cheap to fix early in development rather than being discovered late in the project.