* fix: add missing plugin.json files and restore trailing newlines - Add plugin.json for review-fix-a11y skill - Add plugin.json for free-llm-api skill - Restore POSIX-compliant trailing newlines in JSON index files * feat(engineering): add review-fix-a11y skill (WCAG 2.2 a11y audit + fix) (#375) Adds review-fix-a11y (WCAG 2.2 a11y audit + fix) and free-llm-api skills. Includes: - review-fix-a11y: WCAG 2.2 audit workflow, a11y_audit.py scanner, contrast_checker.py - free-llm-api: ChatAnywhere, Groq, Cerebras, OpenRouter, llm-mux, One API setup - secret_scanner.py upgrade with secrets-patterns-db integration (1,600+ patterns) Co-authored-by: ivanopenclaw223-alt <ivanopenclaw223-alt@users.noreply.github.com> * chore: sync codex skills symlinks [automated] * Revert "feat(engineering): add review-fix-a11y skill (WCAG 2.2 a11y audit + fix) (#375)" This reverts commit49c9f2109f. * chore: sync codex skills symlinks [automated] * Revert "feat(engineering): add review-fix-a11y skill (WCAG 2.2 a11y audit + fix) (#375)" This reverts commit49c9f2109f. * feat(engineering-team): add a11y-audit skill — WCAG 2.2 accessibility audit & fix (#376) Built from scratch (replaces reverted PR #375 contribution). Skill package: - SKILL.md: 1132 lines, 3-phase workflow (scan → fix → verify), per-framework fix patterns (React, Next.js, Vue, Angular, Svelte, HTML), CI/CD integration guide, 20+ issue type coverage - scripts/a11y_scanner.py: static scanner detecting 20+ violation types across HTML/JSX/TSX/Vue/Svelte/CSS — severity-ranked, CI-friendly exit codes - scripts/contrast_checker.py: WCAG contrast calculator with AA/AAA checks, --suggest mode, --batch CSS scanning, named color support - references/wcag-quick-ref.md: WCAG 2.2 Level A/AA criteria table - references/aria-patterns.md: ARIA roles, live regions, keyboard interaction - references/framework-a11y-patterns.md: React, Vue, Angular, Svelte fix patterns - assets/sample-component.tsx: sample file with intentional violations - expected_outputs/: scan report, contrast output, JSON output samples - /a11y-audit slash command, settings.json, plugin.json, README.md Validation: 97.6/100 (EXCELLENT), quality 73.9/100 (B-), scripts 2/2 PASS Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: sync codex skills symlinks [automated] --------- Co-authored-by: Leo <leo@openclaw.ai> Co-authored-by: ivanopenclaw223-alt <ivanopenclaw223@gmail.com> Co-authored-by: ivanopenclaw223-alt <ivanopenclaw223-alt@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.1 KiB
7.1 KiB
Framework-Specific Accessibility Patterns
React / Next.js
Common Issues and Fixes
Image alt text:
// ❌ Bad
<img src="/hero.jpg" />
<Image src="/hero.jpg" width={800} height={400} />
// ✅ Good
<img src="/hero.jpg" alt="Team collaborating in office" />
<Image src="/hero.jpg" width={800} height={400} alt="Team collaborating in office" />
// ✅ Decorative image
<img src="/divider.svg" alt="" role="presentation" />
Form labels:
// ❌ Bad — placeholder as label
<input placeholder="Email" type="email" />
// ✅ Good — explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" placeholder="user@example.com" />
// ✅ Good — aria-label for icon-only inputs
<input type="search" aria-label="Search products" />
Click handlers on divs:
// ❌ Bad — not keyboard accessible
<div onClick={handleClick}>Click me</div>
// ✅ Good — use button
<button onClick={handleClick}>Click me</button>
// ✅ If div is required — add keyboard support
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClick(); }}
>
Click me
</div>
SPA route announcements (Next.js App Router):
// Layout component — announce page changes
'use client';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
export function RouteAnnouncer() {
const pathname = usePathname();
const [announcement, setAnnouncement] = useState('');
useEffect(() => {
const title = document.title;
setAnnouncement(`Navigated to ${title}`);
}, [pathname]);
return (
<div aria-live="assertive" role="status" className="sr-only">
{announcement}
</div>
);
}
Focus management after dynamic content:
// After adding item to list, announce it
const [items, setItems] = useState([]);
const statusRef = useRef(null);
const addItem = (item) => {
setItems([...items, item]);
// Announce to screen readers
statusRef.current.textContent = `${item.name} added to list`;
};
return (
<>
<div ref={statusRef} aria-live="polite" className="sr-only" />
{/* list content */}
</>
);
React-Specific Libraries
@radix-ui/*— accessible primitives (Dialog, Tabs, Select, etc.)@headlessui/react— unstyled accessible componentsreact-aria— Adobe's accessibility hookseslint-plugin-jsx-a11y— lint rules for JSX accessibility
Vue 3
Common Issues and Fixes
Dynamic content announcements:
<template>
<div aria-live="polite" class="sr-only">
{{ announcement }}
</div>
<button @click="search">Search</button>
<ul v-if="results.length">
<li v-for="r in results" :key="r.id">{{ r.name }}</li>
</ul>
</template>
<script setup>
import { ref } from 'vue';
const results = ref([]);
const announcement = ref('');
async function search() {
results.value = await fetchResults();
announcement.value = `${results.value.length} results found`;
}
</script>
Conditional rendering with focus:
<template>
<button @click="showForm = true">Add Item</button>
<form v-if="showForm" ref="formRef">
<label for="name">Name</label>
<input id="name" ref="nameInput" />
</form>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const showForm = ref(false);
const nameInput = ref(null);
watch(showForm, async (val) => {
if (val) {
await nextTick();
nameInput.value?.focus();
}
});
</script>
Vue-Specific Libraries
vue-announcer— route change announcements@headlessui/vue— accessible componentseslint-plugin-vuejs-accessibility— lint rules
Angular
Common Issues and Fixes
CDK accessibility utilities:
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { FocusTrapFactory } from '@angular/cdk/a11y';
@Component({...})
export class MyComponent {
constructor(
private liveAnnouncer: LiveAnnouncer,
private focusTrapFactory: FocusTrapFactory
) {}
addItem(item: Item) {
this.items.push(item);
this.liveAnnouncer.announce(`${item.name} added`);
}
openDialog(element: HTMLElement) {
const focusTrap = this.focusTrapFactory.create(element);
focusTrap.focusInitialElement();
}
}
Template-driven forms:
<!-- ❌ Bad -->
<input [formControl]="email" placeholder="Email" />
<!-- ✅ Good -->
<label for="email">Email address</label>
<input id="email" [formControl]="email"
[attr.aria-invalid]="email.invalid && email.touched"
[attr.aria-describedby]="email.invalid ? 'email-error' : null" />
<div id="email-error" *ngIf="email.invalid && email.touched" role="alert">
Please enter a valid email address.
</div>
Angular-Specific Tools
@angular/cdk/a11y—FocusTrap,LiveAnnouncer,FocusMonitorcodelyzer— a11y lint rules for Angular templates
Svelte / SvelteKit
Common Issues and Fixes
<!-- ❌ Bad — on:click without keyboard -->
<div on:click={handleClick}>Action</div>
<!-- ✅ Good — Svelte a11y warning built-in -->
<button on:click={handleClick}>Action</button>
<!-- ✅ Accessible toggle -->
<button
on:click={() => isOpen = !isOpen}
aria-expanded={isOpen}
aria-controls="panel"
>
{isOpen ? 'Close' : 'Open'} Details
</button>
{#if isOpen}
<div id="panel" role="region" aria-labelledby="toggle-btn">
Panel content
</div>
{/if}
Note: Svelte has built-in a11y warnings in the compiler — it flags missing alt text, click-without-keyboard, and other common issues at build time.
Plain HTML
Checklist for Static Sites
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Descriptive Page Title</title>
</head>
<body>
<!-- Skip link -->
<a href="#main" class="skip-link">Skip to main content</a>
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about" aria-current="page">About</a></li>
</ul>
</nav>
</header>
<main id="main" tabindex="-1">
<h1>Page Heading</h1>
<!-- Only one h1 per page -->
<!-- Heading levels don't skip (h1 → h2 → h3, never h1 → h3) -->
</main>
<footer>
<p>© 2026 Company Name</p>
</footer>
</body>
</html>
CSS Accessibility Patterns
Focus Indicators
/* ❌ Bad — removes focus indicator entirely */
:focus { outline: none; }
/* ✅ Good — custom focus indicator */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* ✅ Good — enhanced for high contrast mode */
@media (forced-colors: active) {
:focus-visible {
outline: 2px solid ButtonText;
}
}
Reduced Motion
/* ✅ Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Screen Reader Only
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}