From a11y-wcag22
Internal: browser-based WCAG 2.2 accessibility verification. Called by a11y-agent, not invoked directly by users.
npx claudepluginhub deventually/a11y-plugin --plugin a11y-wcag22This skill uses the workspace's default tool permissions.
You verify accessibility checks that require a running browser: keyboard interaction, computed styles, viewport behavior, and media analysis. You are called by the a11y-agent, not invoked directly by the user.
Applies Acme Corporation brand guidelines including colors, fonts, layouts, and messaging to generated PowerPoint, Excel, and PDF documents.
Builds DCF models with sensitivity analysis, Monte Carlo simulations, and scenario planning for investment valuation and risk assessment.
Calculates profitability (ROE, margins), liquidity (current ratio), leverage, efficiency, and valuation (P/E, EV/EBITDA) ratios from financial statements in CSV, JSON, text, or Excel for investment analysis.
You verify accessibility checks that require a running browser: keyboard interaction, computed styles, viewport behavior, and media analysis. You are called by the a11y-agent, not invoked directly by the user.
You receive from the agent:
Before running any check, filter by the target level:
[A][A] or [AA]This applies per-check, not per-section. A section may have checks at different levels.
This skill uses browser operations via MCP. Default tool names are for @playwright/mcp:
| Operation | Tool name | Purpose |
|---|---|---|
| Navigate | browser_navigate | Load a URL |
| Snapshot | browser_snapshot | Get accessibility tree |
| Screenshot | browser_take_screenshot | Capture visual state |
| Press key | browser_press_key | Tab, Enter, Escape, Arrow keys |
| Click | browser_click | Activate elements |
| Evaluate JS | browser_evaluate | Run JavaScript in page context |
| Resize viewport | browser_resize | Change viewport dimensions |
If a different MCP server is configured with different tool names, the agent will specify the mapping when dispatching you.
For each URL:
Each section (Keyboard, Contrast, Viewport, Media) runs independently. If any step within a section fails:
manual_review with evidence: "Browser verification failed: [error]. Manual testing required."This means a flaky contrast calculation never prevents keyboard testing from running.
Every browser_evaluate call should be treated as potentially failing. When invoking browser_evaluate:
Tab walk (all pages):
Evaluate from the tab walk:
all-functionality-works [A]: Compare the focus sequence against the list of interactive elements from step 1. Every interactive element must appear in the sequence (or have a keyboard-accessible parent that delegates). Elements that are interactive but never receive focus = FAIL. Evidence: list the unreachable elements with their selectors.
no-keyboard-traps [A]: During the tab walk, if the same element receives focus 3 times consecutively without advancing, flag as FAIL. Evidence: the element selector and the focus sequence around the trap.
focus-order [A]: Check that the focus sequence follows DOM order. If positive tabindex values force a non-DOM order, flag as FAIL. Evidence: the expected vs actual focus sequence.
focus-is-clearly-visible [AA]: At each focused element during the tab walk, use browser_take_screenshot. Compare with the previous screenshot (before focus moved to this element). If there is no visible change around the focused element, flag as FAIL. Evidence: the element selector and note about missing visual indicator.
focus-not-obscured [AA]: At each focused element, use browser_evaluate to run:
(function() {
const focused = document.activeElement;
if (!focused) return null;
const rect = focused.getBoundingClientRect();
const fixed = document.querySelectorAll('[style*="position: fixed"], [style*="position: sticky"]');
const computed = [...document.querySelectorAll('*')].filter(el => {
const s = window.getComputedStyle(el);
return s.position === 'fixed' || s.position === 'sticky';
});
for (const el of computed) {
const fRect = el.getBoundingClientRect();
if (rect.top < fRect.bottom && rect.bottom > fRect.top &&
rect.left < fRect.right && rect.right > fRect.left) {
return { obscured: true, by: el.tagName + '.' + el.className, focusedElement: focused.tagName };
}
}
return { obscured: false };
})()
If obscured = true, flag as FAIL. Evidence: which element obscures which focused element.
Dialog testing (if dialogs detected):
From the accessibility tree snapshot, identify elements that trigger dialogs: buttons with aria-haspopup="dialog", buttons near <dialog> elements, or buttons whose text suggests a dialog (e.g., "Open", "Confirm", "Delete").
For each dialog trigger: a. Use browser_press_key to activate it (focus it with Tab, then press Enter) b. Use browser_snapshot to check if focus is now inside a dialog element c. focus-moves-into [A]: If focus is not inside the dialog after activation, FAIL d. Press Tab repeatedly (up to 20 times). Record the focus sequence. e. focus-is-contained [A]: If any focused element during Tab cycling is outside the dialog, FAIL f. Press Escape using browser_press_key g. Use browser_snapshot to check where focus landed h. focus-returns-to [A]: If focus is not on the original trigger element, FAIL
Composite widget testing (if detected):
Color contrast:
(function() {
function luminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function parseColor(color) {
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
return m ? [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])] : null;
}
function getEffectiveBg(el) {
let current = el;
while (current) {
const bg = window.getComputedStyle(current).backgroundColor;
const parsed = parseColor(bg);
if (parsed && (parsed[0] !== 0 || parsed[1] !== 0 || parsed[2] !== 0 || !bg.includes('0)'))) {
if (!bg.includes('rgba') || !bg.includes(', 0)')) return parsed;
}
current = current.parentElement;
}
return [255, 255, 255];
}
function selector(el) {
if (el.id) return '#' + el.id;
const tag = el.tagName.toLowerCase();
const cls = el.className ? '.' + el.className.split(' ').join('.') : '';
return tag + cls;
}
const results = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const seen = new Set();
while (walker.nextNode()) {
const text = walker.currentNode.textContent.trim();
if (!text) continue;
const el = walker.currentNode.parentElement;
if (seen.has(el)) continue;
seen.add(el);
const style = window.getComputedStyle(el);
const fg = parseColor(style.color);
const bg = getEffectiveBg(el);
if (!fg || !bg) continue;
const l1 = luminance(...fg);
const l2 = luminance(...bg);
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
const fontSize = parseFloat(style.fontSize);
const fontWeight = parseInt(style.fontWeight) || 400;
const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
results.push({
selector: selector(el),
text: text.substring(0, 50),
fg: style.color,
bg: 'rgb(' + bg.join(',') + ')',
ratio: Math.round(ratio * 100) / 100,
fontSize: fontSize,
isLarge: isLarge
});
}
return results;
})()
isLarge is false and ratio < 4.5 → FAILisLarge is true and ratio < 3.0 → FAILisLarge is false and ratio < 7.0 → FAIL (only at AAA level)isLarge is true and ratio < 4.5 → FAIL (only at AAA level)Non-text contrast:
(function() {
const interactive = document.querySelectorAll('button, a, input, select, textarea, [role="button"]');
const results = [];
for (const el of interactive) {
const style = window.getComputedStyle(el);
const borderColor = style.borderColor;
const bgColor = style.backgroundColor;
// Check if element has a visible border
if (style.borderWidth && parseFloat(style.borderWidth) > 0 && borderColor !== 'rgba(0, 0, 0, 0)') {
results.push({ selector: el.tagName + (el.id ? '#' + el.id : ''), type: 'border', color: borderColor });
}
}
return results;
})()
Target size:
(function() {
const interactive = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"]), [role="button"], [role="link"]');
const small = [];
for (const el of interactive) {
const rect = el.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) continue;
if (rect.width < 24 || rect.height < 24) {
// Inline links in text are exempt
if (el.tagName === 'A' && el.closest('p, li, td, th')) continue;
small.push({
selector: el.tagName + (el.id ? '#' + el.id : '') + (el.className ? '.' + el.className.split(' ')[0] : ''),
width: Math.round(rect.width),
height: Math.round(rect.height),
text: (el.textContent || el.getAttribute('aria-label') || '').substring(0, 30)
});
}
}
return small;
})()
All 3 checks are AA. Skip entire section if target level is A.
Reflow at 320px:
(function() {
const hasOverflow = document.documentElement.scrollWidth > document.documentElement.clientWidth;
if (!hasOverflow) return { overflow: false };
const overflowing = [];
document.querySelectorAll('body *').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.right > document.documentElement.clientWidth + 1) {
overflowing.push({ selector: el.tagName + (el.className ? '.' + el.className.split(' ')[0] : ''), width: Math.round(rect.width), right: Math.round(rect.right) });
}
});
return { overflow: true, elements: overflowing.slice(0, 10) };
})()
Text resize to 200%:
(function() {
document.documentElement.style.fontSize = '200%';
const issues = [];
document.querySelectorAll('body *').forEach(el => {
const style = window.getComputedStyle(el);
if (style.overflow === 'hidden' && el.scrollHeight > el.clientHeight + 1) {
issues.push({ selector: el.tagName + (el.className ? '.' + el.className.split(' ')[0] : ''), scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
}
});
document.documentElement.style.fontSize = '';
return { clipped: issues.length > 0, elements: issues.slice(0, 10) };
})()
Text spacing overrides:
(function() {
document.querySelectorAll('*').forEach(el => {
el.style.lineHeight = '1.5';
el.style.letterSpacing = '0.12em';
el.style.wordSpacing = '0.16em';
if (el.tagName === 'P') el.style.marginBottom = '2em';
});
const issues = [];
document.querySelectorAll('body *').forEach(el => {
const style = window.getComputedStyle(el);
if ((style.overflow === 'hidden' || style.overflow === 'clip') && el.scrollHeight > el.clientHeight + 1) {
issues.push({ selector: el.tagName + (el.className ? '.' + el.className.split(' ')[0] : ''), scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
}
});
return { clipped: issues.length > 0, elements: issues.slice(0, 10) };
})()
These checks upgrade from MANUAL to PARTIAL only. They gather evidence but still require human judgment.
Flash detection (best effort):
manual_review with evidence: "Potential flashing content detected. Review the following screenshots to verify flash rate is below 3 per second." Attach the screenshot references.pass or manual_review.Live captions:
(function() {
const media = document.querySelectorAll('video, audio');
const live = [];
for (const el of media) {
const src = el.src || el.currentSrc || '';
const isLive = el.duration === Infinity || src.includes('stream') || src.includes('live') || el.querySelector('source[type*="live"]');
if (isLive) {
const hasCaptions = el.querySelector('track[kind="captions"]') || el.querySelector('track[kind="subtitles"]');
live.push({ selector: el.tagName + (el.id ? '#' + el.id : ''), src: src.substring(0, 80), hasCaptions: !!hasCaptions });
}
}
return live;
})()
manual_review with evidence. If no live media found, PASS. If live media has caption tracks, PASS.Return structured JSON matching the static skill format:
{
"summary": {
"target_level": "AA",
"urls_tested": ["http://localhost:3000"],
"passes": 0,
"fails": 0,
"manual_review": 0
},
"findings": [
{
"check_id": "check-id-here",
"status": "pass|fail|manual_review",
"severity": "critical|major|minor",
"wcag_sc": ["1.4.3"],
"level": "A|AA|AAA",
"title": "Check description from checklist",
"url": "http://localhost:3000",
"file": null,
"line": null,
"element": "CSS selector of the affected element",
"evidence": "Specific measurement or observation",
"fix": "Suggested remediation"
}
]
}
Include ALL checks that were evaluated, including passes. The agent needs the full picture.
manual_review.