Custom components have correct ARIA state attributes
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.
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:
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:2.2 · 4.1.2 — Name, Role, Value
- section-508:2018-refresh · 502.3.3 — Values
Taxons
History
- 2026-04-18·v1.0.0·Initial import from accessibility-wcag·automated