# Custom components have correct ARIA state attributes

- **Pattern:** `ab-000129` (`accessibility-wcag.robust.aria-states`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000129
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000129)

## Why it matters

Custom stateful components — toggles, accordions, tabs, menus — that do not update their ARIA state attributes when visual state changes give screen reader users stale or wrong information. A toggle that shows 'On' visually but reports `aria-checked="false"` tells blind users the opposite of the truth. WCAG 2.2 SC 4.1.2 (Name, Role, Value) is Level A — the requirement to keep programmatic state in sync with visual state is not optional.

## Severity rationale

Low because stale ARIA state attributes misinform rather than completely block — users may still interact with the component — but they violate WCAG 2.2 SC 4.1.2 at Level A and cause consistent misinformation for screen reader users.

## Remediation

Bind ARIA state attributes directly to component state variables so they update atomically with the visual change.

```tsx
export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [expanded, setExpanded] = useState(false)
  const contentId = useId()

  return (
    <div>
      <button
        aria-expanded={expanded}      // stays in sync with state
        aria-controls={contentId}     // connects trigger to content
        onClick={() => setExpanded(e => !e)}
      >
        {title}
      </button>
      <div
        id={contentId}
        hidden={!expanded}            // hidden attribute removes from AT tree when closed
      >
        {children}
      </div>
    </div>
  )
}
```

For every custom stateful component in the codebase, verify the ARIA attribute value is the same expression as the state variable — not a separate boolean that could fall out of sync.

## Detection

- **ID:** `aria-states`
- **Severity:** `low`
- **What to look for:** Enumerate every relevant item. Examine custom components (toggles, tabs, accordions, menus). Check that ARIA state attributes (`aria-checked`, `aria-expanded`, `aria-selected`, `aria-pressed`) are: 1) present, 2) updated when state changes, and 3) kept in sync with visual state.
- **Pass criteria:** At least 1 of the following conditions is met. All custom stateful components expose their state via ARIA attributes, and the attributes are kept in sync with the visual UI state.
- **Fail criteria:** Custom components lack ARIA state attributes, or the attributes are not updated when state changes.
- **Skip (N/A) when:** The project has no custom stateful components.
- **Detail on fail:** Example: `"Toggle component changes appearance on click but aria-checked is not updated. Accordion panel expands but aria-expanded remains false"`
- **Remediation:** Keep ARIA state in sync with UI state:

  ```tsx
  export function Toggle({ checked, onChange }) {
    return (
      <button
        role="switch"
        aria-checked={checked}
        aria-label="Toggle notifications"
        onClick={() => onChange(!checked)}
      >
        {checked ? 'On' : 'Off'}
      </button>
    )
  }

  export function Accordion({ expanded, onToggle }) {
    return (
      <div>
        <button aria-expanded={expanded} onClick={() => onToggle(!expanded)}>
          Section Title
        </button>
        {expanded && <div>Content</div>}
      </div>
    )
  }
  ```

## External references

- wcag 4.1.2
- section-508 502.3.3

Taxons: accessibility

HTML version: https://auditbuffet.com/patterns/ab-000129
