( Robust / WCAG 4.1.2 )

Expandable control is missing aria-expanded

ModerateLevel AWCAG 4.1.2 — Name, Role, Value

What is this issue?

A button or other interactive trigger controls whether a section of content is visible (expanding an accordion panel, opening a dropdown, revealing a “show more” section) but has no aria-expanded attribute reflecting whether that content is currently open. The visible state (content shown or hidden) and the announced state (what a screen reader tells the user about the button) are disconnected, because nothing in the markup communicates the relationship programmatically.

This is distinct from the content itself being hidden or shown correctly with hidden or display: none; the content’s own visibility can be perfectly correct while the trigger button still gives no clue about what state it’s currently in.

Why does this matter?

Sighted users see immediately whether an accordion panel is open: the content is visibly there, or it isn’t. Screen reader users get none of that visual context; the only information they have about a disclosure control’s state is whatever the markup explicitly tells them. Without aria-expanded, a screen reader announces a plain button with no state at all, so the user has no way to know in advance whether pressing it will open something new or close something already open.

This turns every interaction with the control into a guess followed by a re-check: activate it, then re-explore the surrounding area to figure out what changed, rather than making an informed decision up front the way a sighted user does by simply looking at the panel.

Who is affected?

  • Screen reader users: hear a plain “button” with no expanded or collapsed state, and have to activate it and then re-explore the page to discover what changed, turning a one-step interaction into two.
  • Cognitive disabilities: users who rely on a screen reader to reduce reading load lose the up-front certainty a stated state gives them, adding an extra verification step to every accordion or dropdown they interact with.

What users experience

Priya uses JAWS on Windows to compare shipping options on a checkout page built with accordion-style sections. She tabs to a “Shipping details” button and JAWS announces only “Shipping details, button,” nothing about whether the section is currently open or closed. She presses Enter, hoping to reveal the details, but the section was already open from a previous interaction, and her press just closed it. She has to press Enter again and re-listen to confirm she’s back to where she started, on every section she checks.

How do I fix it?

Add aria-expanded="false" to the trigger element when its content is hidden, and update it to aria-expanded="true" the moment the content becomes visible, in the same code path that toggles the content’s own visibility, not as a separate step that can drift out of sync. This works because aria-expanded is a state ARIA attribute purpose-built for exactly this relationship: it tells assistive technology, in the same breath as the button’s name and role, whether the thing it controls is currently open.

Pair aria-expanded with aria-controls pointing at the id of the content it reveals, so assistive technology can also programmatically associate the trigger with what it controls, not just its state. Toggle both the hidden attribute (or equivalent CSS) on the content and the aria-expanded value on the trigger from the same function, so the two attributes can never fall out of sync from a partial update.

Code Examples

Before
<button aria-controls="shipping-panel">Shipping details</button>
<div id="shipping-panel" hidden>Ships in 2 business days.</div>
After
<button aria-controls="shipping-panel" aria-expanded="false">
  Shipping details
</button>
<div id="shipping-panel" hidden>Ships in 2 business days.</div>
function toggleShipping() {
  const button = document.querySelector('[aria-controls="shipping-panel"]');
  const panel = document.getElementById('shipping-panel');
  const isOpen = button.getAttribute('aria-expanded') === 'true';

  button.setAttribute('aria-expanded', String(!isOpen));
  panel.hidden = isOpen;
}

The aria-expanded="false" starting state gives screen reader users the same up-front information a sighted user gets from seeing the panel closed. The toggle function updates aria-expanded and hidden together in one function, so a screen reader user’s announced state and the panel’s actual visibility can never drift apart from a partial update.

Common Mistakes

Mistake: “The panel already uses the hidden attribute, so state is already correct.” hidden correctly removes the panel’s content from the accessibility tree when closed, but it says nothing about the trigger button’s own state. A screen reader user needs the button itself to announce “collapsed” before they decide whether to activate it; hidden alone leaves that announcement missing.

Mistake: “aria-expanded only matters for accordions.” Any control whose job is to reveal or hide content needs it: a custom dropdown menu, a “show more” toggle, a filter panel, a mobile navigation hamburger button. The pattern is identical regardless of what’s being revealed.

Mistake: “Setting aria-expanded once when the page loads is enough.” The whole point of the attribute is that it changes every time the disclosure state changes. A trigger stuck on aria-expanded="false" after the user has opened the panel is worse than having no attribute at all, because it actively tells screen reader users something false.

Mistake: “I can add aria-expanded to a <details>/<summary> pair for extra clarity.” Native <details> already exposes open/closed state to assistive technology automatically, driven by its open attribute. Manually adding aria-expanded on top risks the two falling out of sync and announcing contradictory states; let the native element handle it.

How RedFlag Detects This

Guidance only: RedFlag documents this issue but does not currently flag it; verify manually. This rule id isn’t wired into RedFlag’s rule catalogue, coverage matrix, or any automated detector, manual-check workflow, or AI-review flow; it exists purely as a documentation page today, with no product-side tracking of any kind.

False negative: every occurrence is a false negative in the strict sense that nothing is ever automatically flagged, so a page full of accordion buttons with no aria-expanded at all passes every RedFlag scan silently. False positive: not applicable, since this check never raises an automated violation to begin with. Manual step: tab to every disclosure control on the page and confirm a screen reader announces “expanded” or “collapsed” as part of the button’s own name and role, and that the announced state updates the moment you activate it.

Manual Testing

  1. Open the page in Chrome or Firefox with NVDA, JAWS, or VoiceOver running.
  2. Tab to a control that reveals or hides content: an accordion header, a dropdown toggle, a “show more” button.
  3. Listen for the announced state: it should include “expanded” or “collapsed” as part of the announcement, not just the button’s visible label.
  4. Activate the control and tab back to it (or listen for a live update, depending on your screen reader), confirming the announced state has flipped to match the panel’s new visibility.
  5. If the announcement never mentions expanded/collapsed state, or the state doesn’t update after activation, the check fails.

4.1.2 Name, Role, Value: Every interface component must expose a name, role, and current value, including state, to assistive technology. A disclosure trigger with no aria-expanded has a name and a role but is missing the “value” (state) component of that triad, which is exactly what this rule checks.

Required ARIA attribute is missing covers the broader family of components that fail to expose a required ARIA state or property; aria-expanded on a disclosure trigger is one specific, common instance of that pattern.

Toggle field is missing an accessible name shares the same state-exposure principle applied to aria-pressed toggle buttons instead of expand/collapse controls; both are “does this control announce its own current state” checks.

Modal is missing focus management is a related dynamic-disclosure failure: once a disclosure pattern opens a modal instead of an inline panel, state exposure alone isn’t enough; focus also has to move into it.

Hidden component exposed to screen readers is the inverse mismatch in the same visual-versus-DOM-state family: content whose visual hidden state doesn’t match what’s exposed to assistive technology, rather than a trigger whose announced state doesn’t match its visual one.

Status messages are not announced to assistive tech shares the same underlying principle of keeping a dynamic state change synchronized with what assistive technology announces, applied to live status updates instead of expand/collapse state.

References

Frequently asked questions

Does aria-expanded work on any element, or only on button?

aria-expanded works on any element with a role that supports it: buttons, links, and elements with roles like combobox or tab all support it, per the ARIA specification's role definitions. What matters most is that the element is also genuinely focusable and operable, which native <button> gives you for free and a styled <div> does not.

Should aria-expanded be a string "true"/"false" or a real boolean?

It must be the literal strings "true" or "false", not a JavaScript boolean. ARIA attributes are HTML attributes under the hood, and HTML attribute values are always strings, so setting aria-expanded to the boolean value true in JavaScript coerces it to the string "true" correctly, but writing it as an unquoted boolean in markup is invalid.

Does hiding collapsed content with the hidden attribute still require aria-expanded on the trigger?

Yes. The hidden attribute correctly removes the collapsed content from the accessibility tree, but it says nothing about the trigger button's own state; a screen reader user still needs the button itself to announce "collapsed" or "expanded" before they decide whether to activate it.

Is aria-expanded the same thing as the open attribute on a <details> element?

No, and you should not add aria-expanded to a native <details>/<summary> pair. The browser already exposes open and closed state for <details> automatically based on its open attribute, so adding aria-expanded manually risks the two falling out of sync and contradicting each other.

Does this apply to a custom dropdown menu, or only to accordions?

It applies to any disclosure pattern: accordions, custom select-style dropdowns, expandable filter panels, "show more" toggles, any control whose whole job is to reveal or hide a section of content. The mechanism is identical regardless of what the revealed content looks like.