Accessibility testing with JavaScript

Automated accessibility testing catches issues before they reach production. JavaScript libraries like axe-core integrate directly into your existing test suite — unit tests, integration tests, and end-to-end tests.

axe-core — the foundation

Most JavaScript accessibility testing tools are built on axe-core. It runs WCAG 2.1 checks against rendered DOM and returns structured violations.

Standalone usage

import axe from 'axe-core';

axe.run(document, {
    runOnly: ['wcag2a', 'wcag2aa']
}).then(results => {
    if (results.violations.length > 0) {
        results.violations.forEach(violation => {
            console.error(`[${violation.impact}] ${violation.description}`);
            violation.nodes.forEach(node => {
                console.error(`  Element: ${node.html}`);
                console.error(`  Fix: ${node.failureSummary}`);
            });
        });
    }
});

Running on specific elements

axe.run(document.getElementById('login-form'), {
    rules: {
        'color-contrast': { enabled: true },
        'label': { enabled: true },
        'autocomplete-valid': { enabled: true }
    }
}).then(results => {
    console.log(`${results.violations.length} violations found`);
});

Jest + jest-axe

Add accessibility checks to your React, Vue, or Angular component tests :

npm install --save-dev jest-axe
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('LoginForm', () => {
    it('should have no accessibility violations', async () => {
        const { container } = render(<LoginForm />);
        const results = await axe(container);
        expect(results).toHaveNoViolations();
    });
});

Testing specific rules

it('should have proper form labels', async () => {
    const { container } = render(<ContactForm />);
    const results = await axe(container, {
        rules: {
            'label': { enabled: true },
            'input-button-name': { enabled: true }
        }
    });
    expect(results).toHaveNoViolations();
});

Testing dynamic states

it('should remain accessible after form submission error', async () => {
    const { container, getByText } = render(<LoginForm />);
    
    fireEvent.click(getByText('Submit'));
    
    await waitFor(() => {
        expect(getByText('Email is required')).toBeInTheDocument();
    });

    const results = await axe(container);
    expect(results).toHaveNoViolations();
});

Playwright accessibility testing

Playwright has built-in axe-core integration through @axe-core/playwright :

npm install --save-dev @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Homepage accessibility', () => {
    test('should not have any automatically detectable violations', async ({ page }) => {
        await page.goto('/');
        
        const results = await new AxeBuilder({ page })
            .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
            .analyze();

        expect(results.violations).toEqual([]);
    });
});

Testing specific page sections

test('navigation should be accessible', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
        .include('nav')
        .analyze();

    expect(results.violations).toEqual([]);
});

Testing keyboard navigation

test('should navigate through cards with keyboard', async ({ page }) => {
    await page.goto('/');
    
    await page.keyboard.press('Tab');
    const skipLink = await page.locator(':focus');
    await expect(skipLink).toHaveText('Skip to main content');

    await page.keyboard.press('Tab');
    const firstCard = await page.locator(':focus');
    await expect(firstCard).toHaveAttribute('href');
});

Testing focus management

test('focus should move to heading after navigation', async ({ page }) => {
    await page.goto('/');
    await page.click('a[href="/about"]');

    const focused = await page.evaluate(() => {
        return {
            tag: document.activeElement.tagName,
            text: document.activeElement.textContent
        };
    });

    expect(focused.tag).toBe('H1');
});

Cypress + cypress-axe

npm install --save-dev cypress-axe
import 'cypress-axe';

describe('Product page', () => {
    beforeEach(() => {
        cy.visit('/products');
        cy.injectAxe();
    });

    it('has no detectable accessibility violations', () => {
        cy.checkA11y();
    });

    it('product cards are accessible', () => {
        cy.checkA11y('.product-card');
    });

    it('remains accessible after filtering', () => {
        cy.get('#category-filter').select('Electronics');
        cy.checkA11y();
    });
});

Custom violation handling

cy.checkA11y(null, null, (violations) => {
    violations.forEach(violation => {
        cy.log(`[${violation.impact}] ${violation.id}: ${violation.description}`);
        violation.nodes.forEach(node => {
            cy.log(`${node.html}`);
        });
    });
});

ESLint — catch issues at write time

Catch accessibility issues in JSX/TSX as you type :

npm install --save-dev eslint-plugin-jsx-a11y
{
    "plugins": ["jsx-a11y"],
    "extends": ["plugin:jsx-a11y/recommended"],
    "rules": {
        "jsx-a11y/anchor-is-valid": "error",
        "jsx-a11y/alt-text": "error",
        "jsx-a11y/label-has-associated-control": "error",
        "jsx-a11y/no-autofocus": "warn"
    }
}

This catches errors like missing alt attributes and invalid ARIA props before the code even runs.

CI/CD integration

Add accessibility checks to your pipeline :

name: Accessibility Tests
on: [push, pull_request]

jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run build
      - run: npx serve -l 3000 dist &
      - run: npx wait-on http://localhost:3000
      - run: npm run test:a11y

What automated tests catch vs miss

CatchesMisses
Missing alt textWhether alt text is meaningful
Missing form labelsWhether labels make sense
Color contrast ratiosContrast in custom themes or hover states
Missing ARIA attributesWhether ARIA is used correctly in context
Duplicate IDsLogical tab order
Empty links/buttonsWhether content updates are announced

Rule of thumb : Automated tests cover ~40% of WCAG criteria. Always supplement with manual screen reader testing.

Resources

If you found this helpful, share it with someone who's building for the web.

- Vinay Ranjan