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.
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.
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.
ID: accessibility-wcag.robust.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>
)
}