Audits web pages against WCAG 2.1 A/AA criteria using DOM checks, accessibility snapshots, and keyboard tests to identify barriers and suggest fixes.
From user-testing-agentnpx claudepluginhub ncklrs/claude-chrome-user-testing --plugin user-testing-agentThis skill uses the workspace's default tool permissions.
criteria.jsonProvides Kotlin patterns for JetBrains Exposed ORM: DSL/DAO queries, coroutine transactions, HikariCP pooling, Flyway migrations, repository pattern.
Provides Ktor server patterns for routing DSL, plugins (auth, CORS, serialization), Koin DI, WebSockets, services, and testApplication testing.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
This skill provides WCAG 2.1 accessibility audit capabilities, including criteria definitions, check implementations, and scoring logic.
Systematically evaluate web pages against WCAG 2.1 Level A and AA success criteria to identify accessibility barriers and provide remediation guidance.
WCAG is organized around four principles (POUR):
| Level | Description | Requirements |
|---|---|---|
| A | Minimum | Essential accessibility |
| AA | Standard | Most common requirement |
| AAA | Enhanced | Highest accessibility |
This skill implements Level A and AA checks.
Most WCAG checks can be performed via JavaScript evaluation:
// Example: Check for images without alt text
(function() {
const images = document.querySelectorAll('img');
const violations = [];
images.forEach(function(img) {
if (!img.hasAttribute('alt')) {
violations.push({
element: img.outerHTML.substring(0, 100),
selector: getSelector(img)
});
}
});
return { passed: violations.length === 0, violations: violations };
})();
The accessibility snapshot provides:
Parse the snapshot to verify semantic structure.
Tab through the page to verify:
Requirement: All non-text content has a text alternative.
Check:
// Find images without alt
const imagesWithoutAlt = document.querySelectorAll('img:not([alt])');
// Find inputs type=image without alt
const imageInputsWithoutAlt = document.querySelectorAll('input[type="image"]:not([alt])');
// Find area elements without alt
const areasWithoutAlt = document.querySelectorAll('area:not([alt])');
Pass if: No violations found Severity: Critical (for functional images), Minor (for decorative)
Requirement: Information, structure, and relationships are programmatically determinable.
Check:
// Check heading hierarchy
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
let lastLevel = 0;
const violations = [];
headings.forEach(function(h) {
const level = parseInt(h.tagName[1]);
if (level > lastLevel + 1) {
violations.push({ element: h, issue: 'Skipped heading level' });
}
lastLevel = level;
});
// Check form labels
const inputs = document.querySelectorAll('input, select, textarea');
inputs.forEach(function(input) {
if (!input.labels || input.labels.length === 0) {
if (!input.getAttribute('aria-label') && !input.getAttribute('aria-labelledby')) {
violations.push({ element: input, issue: 'Missing label' });
}
}
});
Severity: Major
Requirement: All functionality is available via keyboard.
Check: Tab through page and verify all interactive elements are reachable.
Severity: Critical
Requirement: Focus can move away from any component.
Check: After focusing each element, verify Tab/Shift+Tab moves focus.
Severity: Critical
Requirement: Mechanism to bypass repeated content.
Check:
// Look for skip links
const skipLink = document.querySelector('a[href^="#main"], a[href^="#content"], .skip-link');
// Look for landmark regions
const landmarks = document.querySelectorAll('main, [role="main"], nav, [role="navigation"]');
return { hasSkipLink: !!skipLink, hasLandmarks: landmarks.length > 0 };
Severity: Major
Requirement: Web pages have titles describing topic or purpose.
Check:
const title = document.title;
return {
hasTitle: title && title.trim().length > 0,
title: title,
isDescriptive: title && title.length > 5 && !title.match(/^(untitled|home|page)$/i)
};
Severity: Minor
Requirement: Default human language is programmatically determinable.
Check:
const htmlLang = document.documentElement.getAttribute('lang');
const validLangs = ['en', 'es', 'fr', 'de', 'it', 'pt', 'zh', 'ja', 'ko', 'ar', 'ru'];
return {
hasLang: !!htmlLang,
lang: htmlLang,
isValid: validLangs.some(function(l) { return htmlLang && htmlLang.startsWith(l); })
};
Severity: Minor
Requirement: Labels or instructions provided for user input.
Check:
const formFields = document.querySelectorAll('input:not([type="hidden"]), select, textarea');
const violations = [];
formFields.forEach(function(field) {
const hasLabel = field.labels && field.labels.length > 0;
const hasAriaLabel = field.getAttribute('aria-label');
const hasAriaLabelledby = field.getAttribute('aria-labelledby');
const hasTitle = field.getAttribute('title');
if (!hasLabel && !hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
violations.push({ element: field, issue: 'No accessible label' });
}
});
Severity: Critical
Requirement: No duplicate IDs, elements properly nested.
Check:
const allIds = document.querySelectorAll('[id]');
const idMap = {};
const duplicates = [];
allIds.forEach(function(el) {
const id = el.id;
if (idMap[id]) {
duplicates.push(id);
}
idMap[id] = true;
});
return { hasDuplicates: duplicates.length > 0, duplicates: duplicates };
Severity: Minor
Requirement: UI components have accessible name, role, and state.
Check via browser_snapshot: Verify custom widgets have appropriate ARIA.
Severity: Critical
Requirement: Text has contrast ratio of at least 4.5:1 (3:1 for large text).
Check:
function getLuminance(r, g, b) {
var a = [r, g, b].map(function(v) {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
function getContrastRatio(l1, l2) {
var lighter = Math.max(l1, l2);
var darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Check text elements
var textElements = document.querySelectorAll('p, span, a, h1, h2, h3, h4, h5, h6, li, td, th, label');
var violations = [];
textElements.forEach(function(el) {
var style = window.getComputedStyle(el);
var color = style.color;
var bgColor = style.backgroundColor;
// ... calculate contrast
});
Severity: Major
Requirement: UI components have 3:1 contrast ratio.
Check: Verify button borders, input borders, focus indicators have sufficient contrast.
Severity: Major
Requirement: Keyboard focus indicator is visible.
Check: Tab through elements and verify focus styles are visible.
Severity: Major
function calculateScore(results) {
var levelA = results.filter(function(r) { return r.level === 'A'; });
var levelAA = results.filter(function(r) { return r.level === 'AA'; });
var aScore = (levelA.filter(function(r) { return r.passed; }).length / levelA.length) * 100;
var aaScore = (levelAA.filter(function(r) { return r.passed; }).length / levelAA.length) * 100;
var overall = (results.filter(function(r) { return r.passed; }).length / results.length) * 100;
return {
levelA: Math.round(aScore),
levelAA: Math.round(aaScore),
overall: Math.round(overall),
grade: getGrade(overall)
};
}
function getGrade(score) {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
| Issue | Fix |
|---|---|
| Missing alt text | Add descriptive alt attribute |
| Missing form labels | Associate label with for attribute |
| Low contrast | Increase color difference |
| No skip link | Add "Skip to main content" link |
| Missing lang | Add lang attribute to html element |
| Keyboard trap | Ensure Tab/Escape can exit |
| No focus visible | Add :focus styles with outline |
Adding alt text:
<!-- Before -->
<img src="chart.png">
<!-- After -->
<img src="chart.png" alt="Sales chart showing 20% growth in Q4">
Adding form label:
<!-- Before -->
<input type="email" placeholder="Email">
<!-- After -->
<label for="email">Email address</label>
<input type="email" id="email">
Adding skip link:
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- ... header/nav ... -->
<main id="main-content">
Improving contrast:
/* Before: 2.5:1 ratio */
.text { color: #999; background: #fff; }
/* After: 4.6:1 ratio */
.text { color: #666; background: #fff; }
When violations are found, annotate them:
See commands/wcag-audit.md for full report format templates.