Skip to content
Back to skills

Web Accessibility

ASecurity

Use this skill when building or reviewing UI that has to be usable by keyboard and screen reader - forms, modals, menus, tabs, tables, toasts, custom controls, dark mode, or a WCAG/508 compliance request. Covers semantics before ARIA, focus management, names and errors, contrast, motion, and how to verify with axe plus a keyboard pass instead of guessing.

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 9, 2026
ai-agentsgonode

Works with

  • cli

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 9, 2026

npx -y skills add chadixearth/graphyloop --skill web-accessibility --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Web Accessibility?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Web Accessibility
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/chadixearth-web-accessibility/badge)](https://www.skillsdirectory.com/skills/chadixearth-web-accessibility)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: web-accessibility
description: Use this skill when building or reviewing UI that has to be usable by keyboard and screen reader - forms, modals, menus, tabs, tables, toasts, custom controls, dark mode, or a WCAG/508 compliance request. Covers semantics before ARIA, focus management, names and errors, contrast, motion, and how to verify with axe plus a keyboard pass instead of guessing.
---

# Web accessibility (WCAG 2.2 AA, implementation-level)

Accessibility fails in a small number of predictable places: a `div` wired with
`onClick`, a modal that leaves focus behind it, an input whose label is only a
placeholder, an error announced in red and nowhere else, and an icon button with no
name. Fix those five and most of an audit disappears.

## When to activate

- Building or changing forms, dialogs, dropdowns, menus, tabs, accordions,
  carousels, data tables, toasts, or any custom control.
- Anything described as "compliance", "WCAG", "508", "screen reader", "keyboard".
- A UI review pass, or a design handoff with hover-only affordances or thin grey text.

## Rule 0: semantics before ARIA

Use the element that already has the behavior. `<button>`, `<a href>`, `<input>`,
`<select>`, `<dialog>`, `<details>`, `<table>`, `<nav>`, `<main>`, `<label>`,
`<fieldset>`. Each brings focusability, keyboard handling, and a role for free.

- `<div onClick>` is not a button: no focus, no Enter/Space, no role. If you must,
  it needs `role="button" tabindex="0"` **and** an Enter+Space handler — three ways
  to reintroduce a bug the native element never had.
- Wrong ARIA is worse than no ARIA. `role="button"` on a link, `aria-hidden` on a
  focusable node, or an `aria-label` that contradicts visible text actively misleads.
- One `<h1>` per page; heading levels never skip. Landmarks (`header`, `nav`,
  `main`, `footer`) present once each, `main` exactly once.

## Keyboard and focus

- Everything interactive is reachable with Tab in visual order and operable with
  Enter (buttons/links), Space (buttons/checkboxes), arrows (radios, tabs, menus,
  listbox), Escape (dismiss).
- Never `outline: none` without a replacement. Visible focus needs ≥ 3:1 contrast
  against the adjacent color; `:focus-visible` is the right hook.
- Dialogs and drawers: move focus in on open (first focusable or the heading), trap
  it while open, restore it to the trigger on close, close on Escape, and mark the
  rest of the page inert (`inert` attribute or `aria-hidden` on the background).
- `tabindex` is `0` or `-1`. Positive values reorder the whole page and are a bug.
- Skip link to `#main` as the first focusable element, visible on focus.
- Never trap focus in a loop the user cannot leave, and never move focus on
  keystroke while typing (that includes "helpful" autofocus on search-as-you-type).

## Names, states, errors

- Every input has a programmatic name: a `<label for>` (preferred), `aria-label`, or
  `aria-labelledby`. Placeholder is not a label — it vanishes on input and often
  fails contrast.
- Icon-only controls get an accessible name (`aria-label="Close"`), and decorative
  icons/SVGs get `aria-hidden="true"`.
- Images: meaningful → describe the meaning; decorative → `alt=""`. Never a filename.
- State goes through the platform: `aria-expanded`, `aria-selected`,
  `aria-checked`, `aria-current`, `aria-pressed`, `disabled`, `aria-invalid`.
  Colour alone is never the signal (WCAG 1.4.1).
- Errors: text next to the field, tied with `aria-describedby`, `aria-invalid="true"`
  on the control, and focus moved to the first error on submit. A summary region
  with `role="alert"` for the count.
- Async status (saving, loaded, toast) announced via a live region
  (`aria-live="polite"`, `role="status"`; `assertive` only for genuine
  interruptions). A spinner with no text announces nothing.
- Required fields marked in text or `required`, not only with a red asterisk.

## Visual, motion, zoom

- Contrast: 4.5:1 body text, 3:1 for ≥ 24px or bold ≥ 19px, 3:1 for icons,
  borders, and focus rings. Check both themes — dark mode regressions are common.
- Target size ≥ 24×24 CSS px (2.5.8), with spacing when smaller is unavoidable.
- Works at 200% zoom and at 320px width with no horizontal scroll; no fixed
  viewport that blocks zoom (`maximum-scale=1` / `user-scalable=no` are violations).
- Respect `prefers-reduced-motion`: no parallax, autoplay, or long transitions when
  set. Nothing flashes more than 3×/second. Autoplaying media has a pause control.
- Hover-only content must also appear on focus, and must be dismissible.
- `lang` on `<html>`, a unique descriptive `<title>` per route.

## Verification (run it, do not assert it)

```
npx playwright test                                # your a11y specs
npm i -D @axe-core/playwright                      # then assert zero violations
npx lighthouse http://127.0.0.1:PORT --only-categories=accessibility --quiet
```

```js
// axe in a Playwright spec — the cheapest real signal
import AxeBuilder from '@axe-core/playwright'
const { violations } = await new AxeBuilder({ page }).withTags(['wcag2a','wcag2aa','wcag22aa']).analyze()
expect(violations, JSON.stringify(violations.map(v => v.id))).toEqual([])
```

Automation catches roughly a third of it. Also do the manual pass:

1. Tab through the whole flow — order sane, focus always visible, nothing skipped
   or trapped.
2. Open and close every dialog/menu with keyboard only; confirm focus returns.
3. Submit a form empty — is the error reachable, named, and announced?
4. Zoom to 200% and narrow to 320px.
5. One screen-reader spot check (NVDA / VoiceOver / Narrator) on the changed flow.

## Reporting

Report `WCAG criterion · component · file:line · what a keyboard or screen-reader
user hits · fix`. Include the axe violation ids you cleared and the keyboard steps
you actually performed. If no browser was available, say the automated pass did not
run — do not present a code read as a verified result.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.