<button
aria-expanded={open}
aria-controls="filters-panel"
onClick={() => setOpen(!open)}
>
Filters
</button>
<div id="filters-panel" hidden={!open}>…</div>
These states tell screen reader users what sighted users see from the visuals.
The common ones:
aria-expanded: on a button that shows or hides something, like an accordion, a dropdown or a mobile menu. It istruewhen open. The screen reader says "Filters, button, expanded".aria-pressed: on a toggle button, like a Bold button or Mute. It istruewhen on. Keep the label the same ("Mute"). Don't change both the label and the state, or users hear "Unmute, pressed" and can't tell what will happen.aria-current="page": on the link to the page you are on, in navigation or breadcrumbs.aria-controls: points to the id of the element the control changes. Screen reader support for it is weak, so treat it as a nice extra, not the main signal.
Keeping them correct: drive the attribute from the same state that drives the visuals. In React, aria-expanded={open} and hidden={!open} read the same open variable. Then they can never disagree. A stale aria-expanded="false" on an open menu is worse than no attribute at all.
aria-expandedtells users whether a disclosure, menu or accordion is open.aria-pressedmarks a toggle button as on or off, with a label that does not change.aria-current="page"marks the current page in navigation.aria-controlshas weak screen reader support, so don't rely on it alone.- Drive ARIA states from the same state as the visuals so they never disagree.