Skip to content
Beta Multi-model support (OpenAI, Gemini, and more) launching soon.

accessibility-aria-review

Installable Ships in accessibility-auditor — or copy the raw skill below.
Open Claude.ai

Paste into Claude Code, Claude.ai, the desktop app, or the browser extension — ⌘/Ctrl+V.

ExperimentalNo security review

Accessibility ARIA Review

Perform a targeted ARIA audit on a component, page, or design-system pattern. Goes deeper than automated tools on role correctness, state lifecycle, keyboard contract, and focus management — areas where incorrect ARIA is actively worse than no ARIA.

Usage

/accessibility-aria-review <path or URL> [--framework react|astro|nextjs|vanilla] [--focus landmarks|widgets|live-regions|keyboard|all]

Examples:

  • /accessibility-aria-review src/components/Modal.tsx --framework react --focus keyboard
  • /accessibility-aria-review src/components/ --focus widgets — audit all components in directory
  • /accessibility-aria-review https://localhost:3000 --focus landmarks

First Rule of ARIA

No ARIA is better than bad ARIA.

Before adding any ARIA attribute, verify whether a native HTML element already provides the semantics. If it does, use the native element. ARIA should only be used to close a gap that HTML cannot fill.

Do this Instead of
<button> <div role="button">
<nav> <div role="navigation">
<ul><li> <div role="list"><div role="listitem">
<h1>–<h6> <div role="heading" aria-level="1">

Phase 1 — Landmark Structure (SC 2.4.1, 1.3.1)

Required landmarks

Every page must have exactly the following landmark structure (or it fails SC 2.4.1):

<header> / role="banner"         — site-wide header (once per page)
<nav> / role="navigation"        — primary navigation (label if multiple)
<main> / role="main"             — primary content (once per page)
<footer> / role="contentinfo"    — site-wide footer (once per page)

Landmark audit checklist

  • Is there exactly one <main> element?
  • Is there exactly one role="banner" / <header> at page level?
  • Are all <nav> elements uniquely labeled with aria-label or aria-labelledby?
  • Does all meaningful content sit inside a landmark (not outside main/aside/nav)?
  • Are <section> elements that contain meaningful sub-regions given an aria-label? (Without a label, <section> has no landmark role.)
  • Is <aside> / role="complementary" used only for supplementary content (not primary content)?
  • Are decorative containers (<div>, <span>) free of spurious landmark roles?

Duplicate landmark handling

When multiple instances of the same landmark exist (two <nav>, multiple <section>), each must have a unique, descriptive aria-label:

<!-- Correct -->
<nav aria-label="Primary">…</nav>
<nav aria-label="Breadcrumb">…</nav>

<!-- Incorrect — AT announces both as "navigation" with no distinction -->
<nav>…</nav>
<nav>…</nav>

Phase 2 — Widget Role Correctness (SC 4.1.2)

Role-to-interaction contract mapping

For each interactive widget found, verify the full ARIA contract:

Widget pattern Role Required attributes Keyboard contract
Accordion button on trigger aria-expanded, aria-controls pointing to panel Enter/Space toggle; no roving tabindex needed
Tab interface tablist, tab, tabpanel aria-selected, aria-controls on tab, aria-labelledby on panel Arrow keys between tabs; Tab to panel content
Modal dialog dialog aria-modal="true", aria-labelledby pointing to heading Focus trap; Escape closes; return focus to trigger on close
Menu / dropdown menu + menuitem aria-haspopup, aria-expanded on trigger Arrow keys navigate items; Tab closes; Escape closes
Combobox combobox aria-expanded, aria-controls, aria-autocomplete Arrow keys select; Enter confirms; Escape closes
Tooltip tooltip aria-describedby on trigger Shows on focus AND hover; not interactive itself
Slider slider aria-valuenow, aria-valuemin, aria-valuemax Arrow keys adjust value
Alert / toast alert — Auto-announced on insertion; no focus movement needed
Progress indicator progressbar aria-valuenow, aria-valuemin, aria-valuemax (or aria-valuetext for indeterminate) Not focusable unless interactive
Tree view tree, treeitem aria-expanded (for parents), aria-selected Arrow keys navigate; Right expands; Left collapses

Common widget errors to check

Accordion:

<!-- Incorrect: div is not keyboard operable -->
<div onclick="toggle()" aria-expanded="false">Section title</div>

<!-- Correct -->
<button aria-expanded="false" aria-controls="panel-1">Section title</button>
<div id="panel-1" role="region" aria-labelledby="trigger-1">…</div>

Tab interface — keyboard trap check:

// Tabs must use arrow keys for navigation, not Tab key
tabList.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowRight') focusNextTab();
  if (e.key === 'ArrowLeft') focusPreviousTab();
  if (e.key === 'Home') focusFirstTab();
  if (e.key === 'End') focusLastTab();
});

Modal — focus trap and return:

// On open: move focus to first focusable element inside dialog
// On close: return focus to the element that triggered the modal
const trigger = document.activeElement;
modal.showModal(); // or manually trap
modal.addEventListener('close', () => trigger.focus());

Phase 3 — Live Region Design (SC 4.1.3)

Live regions announce dynamic content changes to screen readers without requiring focus movement.

Live region types and when to use them

Type Equivalent Interrupts? Use for
role="alert" aria-live="assertive" Yes Errors, urgent warnings
role="status" aria-live="polite" No Success messages, save confirmations, progress updates
aria-live="polite" — No Dynamic content updates (search results count, filter feedback)
aria-live="assertive" — Yes Only for genuinely time-critical updates
role="log" aria-live="polite", aria-relevant="additions" No Chat logs, activity feeds
role="timer" — No Countdown timers (announce sparingly)

Live region audit checklist

  • Is aria-live="assertive" used sparingly? (Overuse is disruptive to screen reader users.)
  • Are live regions pre-existing in the DOM before content is injected? (Adding the element and content simultaneously may not trigger announcement.)
  • Is the live region container empty on load and populated dynamically — or does it contain initial static content that will confuse AT?
  • For form validation errors: are they in a role="alert" container AND linked via aria-describedby to the relevant field?
  • For search result counts: is the live region polite, not assertive?

CMS-specific live region patterns

Sitecore JSS / React:

// Correct pattern: stable container, dynamic content
const [message, setMessage] = React.useState('');
return (
  <div role="status" aria-live="polite" aria-atomic="true">
    {message}
  </div>
);

Optimizely / .NET Razor:

<!-- Render the container server-side; populate client-side -->
<div id="status-region" role="status" aria-live="polite"></div>
<script>
  document.getElementById('status-region').textContent = 'Form saved successfully.';
</script>

Phase 4 — Keyboard Interaction Audit (SC 2.1.1, 2.1.2)

Keyboard audit checklist

  • Tab order — Tab through the entire interactive surface. Does focus move in a logical, left-to-right, top-to-bottom sequence? (See also SC 2.4.3.)
  • No keyboard trap — Tab out of every component. Focus should always be escapable (SC 2.1.2). Exception: modal dialogs must trap focus while open.
  • Custom interactive elements — Any <div> or <span> acting as a button/link? Verify it has: tabindex="0", a keyboard role, and handles both click and keydown (Enter/Space).
  • Focus visible — Is there a visible focus ring on every interactive element? (SC 2.4.7) Fail if outline: none is applied without a custom indicator of equal or greater visibility.
  • Roving tabindex pattern — Used for widget internals (tabs, menu items, radio groups). Only one item in the group is in the tab order at a time; arrow keys navigate within.
  • Skip link — Is "Skip to main content" the first focusable element? Does it actually move focus to <main>?
  • Escape key — Do all overlays, dropdowns, tooltips, and modals close on Escape?

Framework-specific keyboard pitfalls

React:

  • onClick handlers without onKeyDown miss keyboard users.
  • useEffect focus management: call .focus() after the DOM update, not before.
  • Portals (for modals): ensure the portal root is correctly positioned in the DOM for focus trap.

Next.js (App Router):

  • Client-side navigation does NOT move focus automatically. Use router.events or the <FocusAfterNavigation> pattern to return focus to the page <h1> after route change.

Astro islands:

  • Each island is independently hydrated. Ensure island components that include interactive triggers manage focus internally and do not rely on parent page state.

Phase 5 — State Attribute Hygiene (SC 4.1.2)

Audit every use of these state attributes for correctness:

Attribute Valid values Common error
aria-expanded "true" / "false" Set on the trigger, not the panel; never omit when pattern requires it
aria-selected "true" / "false" Required on tab and option roles; "false" must be explicit
aria-checked "true" / "false" / "mixed" "mixed" only for tri-state checkboxes
aria-pressed "true" / "false" / "mixed" Toggle buttons only
aria-hidden "true" / absent Never on focusable elements; always hides from AT
aria-disabled "true" / absent Does NOT prevent keyboard focus (unlike disabled); still announce to AT
aria-controls ID reference Must point to an existing element in the DOM
aria-labelledby ID reference(s) All referenced IDs must exist
aria-describedby ID reference(s) All referenced IDs must exist

Red flags to report

  • aria-hidden="true" on a focusable element — keyboard users reach it, but AT users get silence.
  • aria-expanded not toggled — component JS doesn't update the attribute on open/close.
  • aria-controls pointing to a non-existent ID — broken relationship.
  • aria-live on the wrong element — e.g. on a container that gets replaced, not updated.

Phase 6 — Report Output

Produce a Markdown findings list. Each finding follows the Finding Record schema in accessibility-wcag-scan/templates.md.

Include a summary section specific to ARIA:

## ARIA Review Summary

| Category | Pass | Fail | Not Applicable |
|---|---|---|---|
| Landmark structure | | | |
| Widget role correctness | | | |
| Live regions | | | |
| Keyboard interaction | | | |
| State attribute hygiene | | | |

Note any components where removing ARIA is the correct fix (role added unnecessarily, or conflicting with native HTML semantics).


Reference

Technical details
Plugin
accessibility-auditor
Author
Dakota Smith
Skill name
accessibility-aria-review
Maturity
experimental
Security review
No security review
Owners
  • design
  • engineering
  • security
Argument hint
<component path or URL> [--framework react|astro|nextjs|vanilla] [--focus landmarks|widgets|live-regions|keyboard|all]
Allowed tools
  • Read
  • WebFetch(domain:localhost)
  • WebFetch(domain:*.localhost)
  • WebFetch(domain:www.w3.org)
  • WebFetch(domain:developer.mozilla.org)
  • Bash(cat *)
  • Bash(grep *)
  • Bash(ls *)
  • Bash(find *)
  • Write
  • Edit
Source
plugins/accessibility-auditor/skills/accessibility-aria-review/SKILL.md

Edit this skill on GitHub — opens GitHub's web editor. No git knowledge needed.