audit-accessibility — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited audit-accessibility (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Automated WCAG 2.2 accessibility audit: axe-core scanning, keyboard navigation testing, color contrast verification, ARIA compliance, and assistive technology error analysis. Works with any project — auto-detects pages from the codebase route structure.
Always use the `browser-anti-stall` protocol when using Playwright browser MCP tools.
Test every publicly reachable page. Accessibility bugs on obscure pages still affect real users.
Keyboard-only testing is non-negotiable. If a feature cannot be operated without a mouse, it fails WCAG 2.1.1 regardless of what axe-core says.
Automated tools catch ~30-40% of a11y issues. Always supplement axe-core with manual checks (heading order, focus management, meaningful link text, reading order).
Use concrete WCAG success criteria IDs. "Color contrast is bad" is not actionable. "Fails WCAG 2.2 SC 1.4.3 (Contrast Minimum) — foreground #777 on background #fff = 4.48:1, needs 4.5:1" is.
Glob("**/app/**/page.{tsx,jsx,ts,js}")
Glob("**/pages/**/*.{tsx,jsx,ts,js}")
Glob("**/src/routes/**/*.{tsx,jsx,svelte,vue}")Grep(pattern: "NEXT_PUBLIC_APP_URL|NEXT_PUBLIC_BASE_URL|VITE_APP_URL|APP_URL|localhost", glob: ".env*")If no production URL, default to http://localhost:3000 (or detected dev server port).
Grep(pattern: "tailwindcss|@chakra-ui|@mui|@radix-ui|shadcn|bootstrap|bulma", glob: "package.json")This matters because each framework has different a11y defaults (e.g., Radix primitives are accessible by default, custom components may not be).
Grep(pattern: "axe-core|@axe-core|eslint-plugin-jsx-a11y|pa11y|lighthouse|@testing-library", glob: "package.json")
Grep(pattern: "aria-|role=|tabIndex|sr-only|visually-hidden", glob: "*.{tsx,jsx,vue,svelte}", output_mode: "count")Record:
APP_URL — base URL for testingPAGES — list of routes to auditCSS_FRAMEWORK — Tailwind, MUI, Chakra, etc.COMPONENT_LIB — Radix, shadcn, MUI, Headless UI, etc.EXISTING_A11Y_TOOLS — what's already in placeARIA_USAGE_COUNT — rough indicator of a11y awareness in the codebaseCallMcpTool(server: "user-firecrawl", toolName: "firecrawl_search", arguments: {
"query": "WCAG 2.2 success criteria checklist web accessibility 2026",
"limit": 5
})CallMcpTool(server: "user-firecrawl", toolName: "firecrawl_search", arguments: {
"query": "axe-core automated accessibility testing best practices common false positives",
"limit": 5
})CallMcpTool(server: "user-firecrawl", toolName: "firecrawl_search", arguments: {
"query": "<CSS_FRAMEWORK> <COMPONENT_LIB> accessibility best practices common issues",
"limit": 5
})Scrape the 2-3 most authoritative results:
CallMcpTool(server: "user-firecrawl", toolName: "firecrawl_scrape", arguments: {
"url": "<BEST_RESULT_URL>",
"formats": ["markdown"],
"onlyMainContent": true
})If the project uses a component library, fetch its accessibility documentation:
CallMcpTool(server: "context7", toolName: "resolve-library-id", arguments: {
"libraryName": "<COMPONENT_LIB e.g. radix-ui or chakra-ui>"
})CallMcpTool(server: "context7", toolName: "query-docs", arguments: {
"libraryId": "<RESOLVED_ID>",
"query": "accessibility ARIA keyboard navigation focus management"
})For each page discovered in Phase 0a:
CallMcpTool(server: "user-playwright", toolName: "browser_navigate", arguments: {
"url": "<APP_URL><ROUTE>"
})Apply the browser-anti-stall protocol: wait 2s, snapshot, verify page loaded.
Use browser_evaluate to inject axe-core from CDN and run a full scan:
CallMcpTool(server: "user-playwright", toolName: "browser_evaluate", arguments: {
"javascript": "await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); const results = await axe.run(); return JSON.stringify({ violations: results.violations.map(v => ({ id: v.id, impact: v.impact, description: v.description, help: v.help, helpUrl: v.helpUrl, nodes: v.nodes.length, targets: v.nodes.slice(0, 3).map(n => n.target[0]) })), passes: results.passes.length, incomplete: results.incomplete.length, inapplicable: results.inapplicable.length });"
})For each violation returned, record:
color-contrast, image-alt, label)axe-core cannot catch everything. For each page, also check:
Heading Hierarchy (WCAG 1.3.1):
CallMcpTool(server: "user-playwright", toolName: "browser_evaluate", arguments: {
"javascript": "return JSON.stringify(Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6')).map(h => ({ tag: h.tagName, text: h.textContent.trim().substring(0, 50) })));"
})Verify: only one h1, headings don't skip levels (h1 -> h3 without h2).
Image Alt Text (WCAG 1.1.1):
CallMcpTool(server: "user-playwright", toolName: "browser_evaluate", arguments: {
"javascript": "return JSON.stringify(Array.from(document.querySelectorAll('img')).map(img => ({ src: img.src.substring(img.src.lastIndexOf('/') + 1), alt: img.alt, hasAlt: img.hasAttribute('alt'), decorative: img.getAttribute('role') === 'presentation' || img.alt === '' })));"
})Verify: all informative images have descriptive alt text, decorative images have alt="" or role="presentation".
Link Text (WCAG 2.4.4):
CallMcpTool(server: "user-playwright", toolName: "browser_evaluate", arguments: {
"javascript": "return JSON.stringify(Array.from(document.querySelectorAll('a')).filter(a => { const text = (a.textContent || '').trim().toLowerCase(); return text === 'click here' || text === 'here' || text === 'read more' || text === 'learn more' || text === '' || text === 'link'; }).map(a => ({ href: a.href, text: (a.textContent || '').trim(), ariaLabel: a.getAttribute('aria-label') })));"
})Flag generic link text ("click here", "read more", empty links without aria-label).
Form Labels (WCAG 1.3.1, 4.1.2):
CallMcpTool(server: "user-playwright", toolName: "browser_evaluate", arguments: {
"javascript": "return JSON.stringify(Array.from(document.querySelectorAll('input,select,textarea')).map(el => ({ type: el.type, name: el.name, id: el.id, hasLabel: !!document.querySelector('label[for=\"' + el.id + '\"]'), ariaLabel: el.getAttribute('aria-label'), ariaLabelledBy: el.getAttribute('aria-labelledby'), placeholder: el.placeholder })));"
})Verify: every form control has an associated <label>, aria-label, or aria-labelledby.
Language Attribute (WCAG 3.1.1):
CallMcpTool(server: "user-playwright", toolName: "browser_evaluate", arguments: {
"javascript": "return JSON.stringify({ htmlLang: document.documentElement.lang, htmlDir: document.documentElement.dir });"
})Verify: <html> has a valid lang attribute.
For each page, capture a screenshot as visual evidence:
CallMcpTool(server: "user-playwright", toolName: "browser_take_screenshot", arguments: {})For each page, test that all interactive elements are reachable via Tab key:
CallMcpTool(server: "user-playwright", toolName: "browser_navigate", arguments: {
"url": "<APP_URL><ROUTE>"
})Press Tab repeatedly and snapshot after each press to track focus movement:
CallMcpTool(server: "user-playwright", toolName: "browser_press_key", arguments: {
"key": "Tab"
})CallMcpTool(server: "user-playwright", toolName: "browser_snapshot", arguments: {})After each Tab, check:
Repeat Tab at least 10-15 times per page to cover the main interactive elements.
For each interactive component (dropdowns, modals, tabs, accordions):
Enter/Space activation (WCAG 2.1.1):
Escape to close (WCAG 2.1.1 for modals/dialogs):
CallMcpTool(server: "user-playwright", toolName: "browser_press_key", arguments: {
"key": "Escape"
})Verify: modals/dropdowns close, focus returns to the trigger element.
Arrow key navigation (for tab panels, menus, listboxes):
CallMcpTool(server: "user-playwright", toolName: "browser_press_key", arguments: {
"key": "ArrowDown"
})Test that focus is managed correctly after dynamic updates:
CallMcpTool(server: "plugin-sentry-sentry", toolName: "search_issues", arguments: {
"organizationSlug": "<ORG_SLUG>",
"projectSlug": "<PROJECT_SLUG>",
"query": "is:unresolved aria OR screenreader OR voiceover OR talkback OR nvda OR jaws OR focus OR tabindex OR keyboard"
})CallMcpTool(server: "plugin-sentry-sentry", toolName: "search_issues", arguments: {
"organizationSlug": "<ORG_SLUG>",
"projectSlug": "<PROJECT_SLUG>",
"query": "is:unresolved keydown OR keyup OR keypress OR focusin OR focusout"
})Check if errors disproportionately affect specific browsers or devices that assistive technology users favor:
CallMcpTool(server: "plugin-sentry-sentry", toolName: "get_issue_tag_values", arguments: {
"organizationSlug": "<ORG_SLUG>",
"issueId": "<ISSUE_ID>",
"tagKey": "browser"
})Supplement runtime testing with static analysis of the source code.
Grep(pattern: "onClick(?!.*role=)(?!.*tabIndex)", glob: "*.{tsx,jsx}")Flag div or span elements with click handlers but no role or tabIndex — these are invisible to keyboard and screen reader users.
SemanticSearch(query: "Where are custom interactive components defined (dropdowns, modals, tabs, accordions) that don't use a component library?", target_directories: [])Custom components are the highest risk for a11y issues. Check each for:
role="dialog", role="tabpanel", etc.)onKeyDown)useRef + .focus())aria-expanded, aria-selected, aria-hidden)Grep(pattern: "text-gray-[345]|text-slate-[345]|text-zinc-[345]|text-neutral-[345]|color:\\s*#[789abc]", glob: "*.{tsx,jsx,css,scss}")Light gray text on white background is the most common contrast failure. Flag files using light gray text colors for manual contrast checking.
═══════════════════════════════════════════════════════
WCAG 2.2 ACCESSIBILITY AUDIT REPORT
Project: <PROJECT_NAME>
Date: <DATE>
Standard: WCAG 2.2 Level AA
Pages Tested: <COUNT>
═══════════════════════════════════════════════════════
## 1. EXECUTIVE SUMMARY
| Metric | Result |
|---------------------------|-----------------|
| Pages tested | X |
| Total violations | N |
| Critical violations | N |
| Serious violations | N |
| axe-core rules passed | N |
| Keyboard navigable pages | X/Y |
| Overall compliance | X% (estimated) |
## 2. AXE-CORE SCAN RESULTS (per page)
| Page | Critical | Serious | Moderate | Minor | Pass | Score |
|------|----------|---------|----------|-------|------|-------|
| / | ... | ... | ... | ... | ... | A-F |
| /about | ... | ... | ... | ... | ... | A-F |
## 3. VIOLATIONS BY WCAG CRITERION
| WCAG SC | Name | Level | Pages Affected | Elements | Impact | Status |
|---------|------|-------|----------------|----------|--------|--------|
| 1.1.1 | Non-text Content | A | ... | ... | ... | PASS/FAIL |
| 1.3.1 | Info and Relationships | A | ... | ... | ... | PASS/FAIL |
| 1.4.3 | Contrast (Minimum) | AA | ... | ... | ... | PASS/FAIL |
| 2.1.1 | Keyboard | A | ... | ... | ... | PASS/FAIL |
| 2.4.3 | Focus Order | A | ... | ... | ... | PASS/FAIL |
| 2.4.4 | Link Purpose | A | ... | ... | ... | PASS/FAIL |
| 2.4.7 | Focus Visible | AA | ... | ... | ... | PASS/FAIL |
| 3.1.1 | Language of Page | A | ... | ... | ... | PASS/FAIL |
| 4.1.2 | Name, Role, Value | A | ... | ... | ... | PASS/FAIL |
## 4. KEYBOARD NAVIGATION
| Page | Tab Order Logical | Focus Visible | No Focus Trap | Skip Link | Score |
|------|-------------------|---------------|---------------|-----------|-------|
| / | ... | ... | ... | ... | A-F |
## 5. INTERACTIVE COMPONENTS
| Component | Location | ARIA Roles | Keyboard Operable | Focus Managed | Score |
|-----------|----------|------------|-------------------|---------------|-------|
| Modal | ... | ... | ... | ... | A-F |
| Dropdown | ... | ... | ... | ... | A-F |
## 6. SENTRY ASSISTIVE TECHNOLOGY ERRORS
| Issue | Error Type | Events | Browser/AT | Fix Needed |
|-------|------------|--------|------------|------------|
| ... | ... | ... | ... | ... |
## 7. STATIC CODE FINDINGS
| Pattern | Files | Count | Risk |
|---------|-------|-------|------|
| onClick without role/tabIndex | ... | N | High |
| Custom interactive without ARIA | ... | N | High |
| Low-contrast text classes | ... | N | Medium |
## 8. CRITICAL FINDINGS (Action Required)
P0 — Level A violations (legal risk):
1. [finding with WCAG SC, element, page, evidence]
P1 — Level AA violations (compliance gap):
1. [finding with WCAG SC, element, page, evidence]
P2 — Best practice improvements:
1. [finding with rationale]
## 9. RECOMMENDATIONS
| # | WCAG SC | Current | Recommended | Effort | Files to Change |
|---|---------|---------|-------------|--------|-----------------|
| 1 | ... | ... | ... | S/M/L | ... |
═══════════════════════════════════════════════════════~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.