Sechno
Web Development

Continuous AI for Accessibility: Practical Patterns to Automate Detection and Remediation

A practical guide to adding continuous, AI-assisted accessibility checks into CI/CD. Learn architecture patterns, concrete implementations with axe-core and Playwright, PR automation, and tradeoffs to keep false positives and privacy under control.

SSechno Team 6 min read 82 views
Continuous AI for Accessibility: Practical Patterns to Automate Detection and Remediation

Why continuous accessibility matters now

Accessibility issues are often introduced and left unfixed because the feedback loop is slow: manual audits happen too late, and developers lack actionable, context-rich guidance in pull requests. Recent work on continuous AI for accessibility highlights how automated feedback can accelerate fixes and improve inclusion. This article shows practical, implementable patterns for adding continuous accessibility checks and AI-assisted suggestions into your CI pipeline.

High-level architecture

  • Static checks — linters and static analyzers (HTML semantics, ARIA rules) run fast and gate many issues.
  • Automated UI checks — headless browser runs (Playwright/Puppeteer) with axe-core capture runtime accessibility issues for pages/components.
  • AI summarization and remediation hints — a model summarizes findings, groups duplicates and suggests code-level changes (snippets or patterns) for the PR author.
  • Human review and triage — maintainers validate fixes and adjust the AI’s behavior. Humans keep final control.

Concrete CI flow (practical)

  1. Run fast static checks on every push (HTML, CSS linters).
  2. On PRs, run headless browser checks against the deployed preview (or storybook component preview).
  3. Persist axe-core JSON results and run a small processor that groups issues by type and selector.
  4. Optionally call an AI summarizer to convert the raw results into short, prioritized suggestions and example fixes.
  5. Post a structured comment on the PR with actionable items and links to the failing pages/components.

Why run against preview builds

Running against a preview environment (netlify, Vercel, GitHub Pages, or a staging container) exercises real rendering, dynamic DOM changes and component states that static analysis misses.

Example: Playwright + axe-core runner

Below is a practical runner you can add to a job in CI. It visits a list of pages and emits axe results as JSON. Put this file in your repo and run it from a Node job in your CI. (Escape characters shown as needed for in-repo scripts.)

#!/usr/bin/env node
 
// script: run-axe.js
// Usage: node run-axe.js https://preview.example.com /path/to/output.json
 
const { chromium } = require('playwright');
const AxeBuilder = require('@axe-core/playwright').default;
const fs = require('fs');
 
(async () => {
  const base = process.argv[2];
  const out = process.argv[3] || 'axe-results.json';
  if (!base) {
    console.error('Usage: node run-axe.js https://preview url output.json');
    process.exit(2);
  }
 
  const pages = ['/', '/signup', '/dashboard']; // adjust or generate from sitemap
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const results = [];
 
  for (const path of pages) {
    const page = await context.newPage();
    await page.goto(new URL(path, base).toString(), { waitUntil: 'networkidle' });
    const axe = new AxeBuilder({ page });
    const res = await axe.analyze();
    results.push({ url: new URL(path, base).toString(), violations: res.violations });
    await page.close();
  }
 
  await browser.close();
  fs.writeFileSync(out, JSON.stringify(results, null, 2));
  console.log('Wrote', out);
})();

Sample GitHub Actions job

This job installs Node, runs the Playwright+axe script, and uploads the results as an artifact. In a later step you can call a processor that posts a PR comment.

name: Accessibility CI
 
on:
  pull_request:
    types: [opened, synchronize, reopened]
 
jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Run preview build
        run: npm run build && npm run start:preview & sleep 5
      - name: Run axe checks
        run: node scripts/run-axe.js http://127.0.0.1:3000 axe-results.json
      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: axe-results
          path: axe-results.json

Post results to PR with structured comment

Create a small processor that reads axe-results.json, groups violations by rule and selector, and creates a concise summary. Use the GitHub REST API to post a comment. Below is a minimal Node example; store your GH token in the job as GITHUB_TOKEN or a secrets variable.

// script: comment-a11y.js
// Usage: node comment-a11y.js axe-results.json GITHUB_TOKEN PR_NUMBER REPO
 
const fs = require('fs');
const fetch = require('node-fetch');
 
(async () => {
  const [file, token, pr, repo] = process.argv.slice(2);
  const data = JSON.parse(fs.readFileSync(file, 'utf8'));
  const flattened = [];
  for (const page of data) {
    for (const v of page.violations) {
      flattened.push({rule: v.id, impact: v.impact, help: v.help, nodes: v.nodes.map(n => n.target.join(' | ')), url: page.url});
    }
  }
  if (flattened.length === 0) {
    console.log('No violations found');
    return;
  }
 
  // Build a short markdown-friendly comment
  let body = 'Automated accessibility scan results:\n\n';
  const top = flattened.slice(0, 10);
  for (const f of top) {
    body += `- **${f.rule}** (${f.impact}) on ${f.url}: ${f.help} (example selectors: ${f.nodes.slice(0,2).join(', ')})\n`;
  }
  body += '\nRun the full report artifact for details.';
 
  const url = `https://api.github.com/repos/${repo}/issues/${pr}/comments`;
  await fetch(url, {
    method: 'POST',
    headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ body })
  });
  console.log('Posted comment');
})();

AI-assisted summarization: how to add value (without full automation)

Use an AI model only as a helper for summarization and suggested code snippets, not as an automatic fixer. The pattern below shows a lightweight flow:

  • Input: grouped axe results (rule id, impact, example snippet, failing URL).
  • Prompt: ask the model to prioritize by impact and give 2–3 short remediation steps with example HTML/CSS/ARIA snippets.
  • Output: attach the AI summary to the PR comment under a clearly labeled "AI suggestions" block so humans can accept or edit.

Example minimal call (replace MODEL_ENDPOINT and API_KEY with your credentials):

const fetch = require('node-fetch');
 
async function summarizeWithAI(groupedResults) {
  const prompt = `You are an accessibility engineer. Summarize these grouped accessibility issues and provide 1-3 prioritized, actionable remediation steps with short code snippets.\n\nIssues:\n${JSON.stringify(groupedResults, null, 2)}`;
 
  const res = await fetch('https://MODEL_ENDPOINT/v1/generate', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${process.env.AI_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt, max_tokens: 400 })
  });
  const data = await res.json();
  return data.choices?.[0]?.text || data.output || 'No summary produced.';
}

Tradeoffs and best practices

  • False positives: Automated tools often flag issues that are false positives for your app state. Group by selector and always show an example DOM snapshot to triage quickly.
  • Privacy: Preview builds might expose PII. Mask or avoid testing pages with sensitive data, and avoid sending raw HTML to third-party AI services unless you have explicit controls and consent.
  • Model drift and hallucination: AI suggestions can be imprecise. Always mark AI outputs clearly and require human approval before code changes are merged.
  • Performance: Running full-site checks on every commit can be costly. Run lightweight smoke tests on every push and full scans on scheduled runs or when a PR targets critical areas.

Actionable checklist to get started

  1. Add axe-core checks to a headless browser runner (Playwright/Puppeteer).
  2. Run checks against preview builds created by your PRs.
  3. Post structured results to PRs and attach artifacts for full reports.
  4. Optionally add AI summarization as a separate step; never let AI auto-commit fixes without review.
  5. Iterate on rules and thresholds to reduce noise; maintain a whitelist for known false positives.

Conclusion

Continuous AI for accessibility can close the feedback loop and make accessibility work part of normal PR workflows. The safest, most effective pattern combines deterministic checks (axe-core, linters), preview builds, human review, and optional AI-assisted summaries that help developers act faster. Start small, measure noise, and keep humans in the loop.

Further reading

See the original GitHub discussion of continuous AI for accessibility for broader context: GitHub Blog: Continuous AI for accessibility.

Was this helpful?

Share this post

Comments (0)

Want to join the conversation?

Log in or sign up to leave a comment and share your thoughts.

Log in to Comment