# Accessibility Audit

> Audit and fix React component accessibility issues — ARIA labels, keyboard navigation, colour contrast, semantic HTML, screen reader support. Works with any AI assistant.

- Skill: `kirti/accessibility-audit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kirti/accessibility-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kirti/accessibility-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: kirti (https://skillmd.com/u/kirti)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kirti/accessibility-audit

---


# Accessibility Audit Skill

## When to Use This Skill
- Auditing React components for WCAG 2.1 compliance
- Adding ARIA labels, roles, and descriptions
- Fixing keyboard navigation issues
- Making components screen-reader friendly
- Before shipping any public-facing UI
- Code review for accessibility issues

## The Core Principle
Every interactive element must be:
1. **Perceivable** — visible or audible to all users
2. **Operable** — usable with keyboard alone
3. **Understandable** — clear labels and error messages
4. **Robust** — works with assistive technologies

---

## Rule 1 — Images Must Have Alt Text

```jsx
// ❌ Missing alt text — screen readers say nothing
<img src="/avatar.png" />

// ✅ Descriptive alt text
<img src="/avatar.png" alt="Profile photo of Kirti Kaushal" />

// ✅ Decorative image — empty alt tells screen reader to skip
<img src="/divider.png" alt="" role="presentation" />
```

**AI instruction:** When generating image elements, ALWAYS include alt prop. If decorative, use `alt=""`.

---

## Rule 2 — Buttons Must Have Labels

```jsx
// ❌ Icon-only button — screen reader says "button"
<button onClick={close}><CloseIcon /></button>

// ✅ With aria-label
<button onClick={close} aria-label="Close dialog"><CloseIcon /></button>

// ✅ With visually hidden text
<button onClick={close}>
  <CloseIcon aria-hidden="true" />
  <span className="sr-only">Close</span>
</button>
```

**AI instruction:** Icon-only buttons MUST have `aria-label`. Never generate a button with only an icon and no text alternative.

---

## Rule 3 — Forms Need Labels

```jsx
// ❌ No label — screen reader can't identify field
<input type="email" placeholder="Enter email" />

// ✅ Explicit label
<label htmlFor="email">Email address</label>
<input id="email" type="email" placeholder="your@email.com" />

// ✅ aria-label when visual label not possible
<input
  type="search"
  aria-label="Search products"
  placeholder="Search..."
/>

// ✅ Error messages linked with aria-describedby
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={hasError}
/>
{hasError && <span id="email-error" role="alert">Invalid email</span>}
```

**AI instruction:** Every form input MUST have either a `<label>` with matching `htmlFor`/`id`, or an `aria-label`. Placeholder text is NOT a label.

---

## Rule 4 — Interactive Elements Need Focus Management

```jsx
// ❌ div as button — not keyboard accessible
<div onClick={handleClick} className="btn">Click me</div>

// ✅ Use semantic button
<button onClick={handleClick} className="btn">Click me</button>

// ✅ If div must be interactive
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => e.key === 'Enter' && handleClick()}
>
  Click me
</div>
```

**AI instruction:** Never generate `onClick` on a `div` or `span` without also adding `role="button"`, `tabIndex={0}`, and `onKeyDown` handler.

---

## Rule 5 — Modals/Dialogs Need Focus Trap

```jsx
// ✅ Dialog with proper accessibility
function Modal({ isOpen, onClose, title, children }) {
  const firstFocusRef = useRef(null);

  useEffect(() => {
    if (isOpen) firstFocusRef.current?.focus();
  }, [isOpen]);

  return isOpen ? (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      onKeyDown={(e) => e.key === 'Escape' && onClose()}
    >
      <h2 id="modal-title">{title}</h2>
      <button ref={firstFocusRef} onClick={onClose} aria-label="Close modal">×</button>
      {children}
    </div>
  ) : null;
}
```

---

## Rule 6 — Colour Contrast

```css
/* ❌ Fails WCAG AA (contrast ratio < 4.5:1) */
.text { color: #999; background: #fff; }

/* ✅ Passes WCAG AA (contrast ratio > 4.5:1) */
.text { color: #595959; background: #fff; }

/* ✅ Large text only needs 3:1 ratio */
.heading { color: #767676; font-size: 1.5rem; font-weight: bold; }
```

**AI instruction:** Text colours must meet WCAG AA contrast. Never use light grey on white for body text.

---

## Rule 7 — Live Regions for Dynamic Content

```jsx
// ❌ Status update — screen reader doesn't announce it
<div className="status">{statusMessage}</div>

// ✅ aria-live announces changes automatically
<div aria-live="polite" aria-atomic="true">{statusMessage}</div>

// ✅ For urgent alerts
<div role="alert">{errorMessage}</div>
```

---

## Companion Script
```bash
npx reactforge check-accessibility ./src
```
Scans all JSX/TSX files and reports accessibility violations with line numbers.

---

## Quick Reference Checklist
- [ ] All images have alt text (or `alt=""` if decorative)
- [ ] All buttons have text or `aria-label`
- [ ] All form inputs have `<label>` or `aria-label`
- [ ] No `onClick` on non-interactive elements without keyboard support
- [ ] Modals trap focus and close on Escape
- [ ] Dynamic content uses `aria-live` or `role="alert"`
- [ ] Colour contrast meets WCAG AA (4.5:1 for text)
- [ ] Page has a single `<h1>`, heading hierarchy is correct

