Files
claude-skills-reference/engineering-team/a11y-audit/references/framework-a11y-patterns.md
Alireza Rezvani a059113c96 Dev (#377)
* 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 commit 49c9f2109f.

* chore: sync codex skills symlinks [automated]

* Revert "feat(engineering): add review-fix-a11y skill (WCAG 2.2 a11y audit + fix) (#375)"

This reverts commit 49c9f2109f.

* 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>
2026-03-18 08:42:53 +01:00

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 components
  • react-aria — Adobe's accessibility hooks
  • eslint-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 components
  • eslint-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/a11yFocusTrap, LiveAnnouncer, FocusMonitor
  • codelyzer — 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>&copy; 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;
}