skip to content

Roles, States, and Properties

ARIA has three distinct kinds of attribute — roles say what a thing is, states say what it is doing now, properties describe fixed relationships. Being able to sort aria-expanded from aria-haspopup is a fast signal that you have built a real widget.

part ofHTMLoverview, primer and where to startread it →
on this pageshow

questions

5

In a disclosure widget where a button shows and hides a panel, what does aria-expanded communicate, which element must carry it, and what do its values mean?

level: juniorimportance: must knowfreq 64%

answer

  1. it lives on the trigger, not the panel
  2. three conditions, not two
  3. strings, not HTML boolean presence
  4. absent means "not expandable at all"
  5. visibility and attribute flip together

basics

~20 s

aria-expanded reports whether the content a control governs is currently open. It belongs on the interactive control itself, not on the panel, and takes the string values "true" or "false" — omitting it means the control is not expandable at all.

solid answer

~40 s

`aria-expanded` is a state that tells assistive technology whether the thing this control opens is currently showing. It goes on the **control** — the `<button>` the user activates — never on the panel being revealed, because the user encounters the button first and needs to know what activating it will do. The values are the strings `"true"` and `"false"`; a screen reader typically announces the button as "expanded" or "collapsed". Leaving the attribute off entirely is *not* the same as `"false"`: absent means the control has no expandable relationship at all, so nothing is announced. The obligation this creates is synchronisation — every time your code shows or hides the panel it must flip the attribute in the same step. A stale `aria-expanded="false"` on an open menu is worse than never having added it.

go deeper

for a junior

Remember the one-line rule: aria-expanded goes on the button the user presses, holds the string "true" or "false", and must change whenever the panel opens or closes.

for a middle

Explain the three-condition model — true, false, and absent meaning not expandable — and why the attribute must be updated in the same code path that changes visibility.

for a senior

Demonstrate that you audit for drift: describe how you keep the announced state and the visible state from diverging in a component library, and what you check in the accessibility tree to confirm it.

for a principal

Frame stale ARIA state as a class of defect rather than a one-off bug, and be ready to say how a component API and its tests should make the disclosure contract impossible to get wrong across many teams.

## The pattern A disclosure is the simplest interactive widget on the web: a trigger that shows and hides a region. Accordions, "show more" toggles, navigation submenus and the button that opens a dropdown are all disclosures. The markup is small: ```html <button aria-expanded="false" aria-controls="shipping">Shipping details</button> <div id="shipping" hidden> <p>Ships in 2-3 business days.</p> </div> ``` A sighted user sees the panel appear and the chevron rotate. Someone using a screen reader gets none of that; the only channel available is what the accessibility tree says about the button. `aria-expanded` is that channel. ## What it communicates The state answers one question: is the content this control governs currently displayed? Screen readers fold it into the button's announcement — typically something like "Shipping details, button, collapsed". It also tells the user, before they act, that this button *is* a toggle rather than a link or a submit. That second job is why the third possible condition matters. ## Three conditions, not two - `aria-expanded="true"` — the controlled content is showing. - `aria-expanded="false"` — the control is expandable and the content is hidden. - **attribute absent** — the default, `undefined` in the specification's terms: this control is not expandable. Nothing is announced. That last case is the one people get wrong. Removing the attribute when the panel closes does not mean "collapsed", it means "not a disclosure", and the user loses the affordance entirely. Set it to the string `"false"` instead. Equally, these are string values in an attribute, not HTML boolean attributes: the mere presence of `aria-expanded` does not mean true, and `aria-expanded="false"` is genuinely false. ## It belongs on the control The most common structural mistake is putting the state on the region: ```html <!-- wrong: the state is on the thing being revealed --> <button>Shipping details</button> <div id="shipping" aria-expanded="false" hidden>...</div> ``` Nothing announces that. The user reaches the button, not the hidden panel, and the button is where a decision gets made. Put the state on whatever the user activates. Where the trigger and the region are far apart in the DOM, `aria-controls` on the button can point at the region's `id`, though support for that hint varies across screen readers, so treat it as supplementary rather than load-bearing. `aria-expanded` is not universal across roles — it is supported on roles such as `button`, `link`, `combobox`, `tab`, `treeitem`, `row`, `menuitem` and `gridcell`. It is meaningless on a plain container, and it should never be added to a button that merely navigates or submits, because that announces an open/close affordance that does not exist. ## The synchronisation obligation ARIA is inert. Setting `aria-expanded="true"` reveals nothing and hides nothing; showing and hiding is done by the `hidden` attribute, by CSS, or by removing the node. That means the visible state and the announced state are two separate facts that your code must change together, in the same handler, every time. When they drift, the accessibility tree confidently reports a lie, and a lie is harder to work around than silence — the user has no reason to distrust it. If your toggle logic has one place that changes visibility, put the attribute update in that same place rather than scattering it across call sites. One more nuance: when a panel is hidden with the `hidden` attribute or `display: none`, its contents are removed from the accessibility tree, which is correct — a closed panel's contents should not be reachable. Hiding it visually with something that leaves it in the tree, so screen reader users can still tab into a "closed" panel, is the mirror-image bug and one interviewers like to probe. ## What good looks like A native `<button>` for the trigger, so keyboard activation, focusability and the button role come free; `aria-expanded` on that button, initialised in the HTML rather than only by script; the panel hidden in a way that removes it from the tree; and exactly one code path that flips both visibility and the attribute together.

  • Why is removing aria-expanded when the panel closes not equivalent to setting it to "false"?
    Because the absent state means `undefined` — this control is not expandable. Screen readers then announce a plain button and the user never learns it toggles anything. `aria-expanded="false"` says something different and more useful: this is a disclosure, and it is currently closed. Keep the attribute present at all times and only change its value.
  • Does setting aria-expanded="true" make the panel appear?
    No. ARIA never changes behaviour or rendering; it only changes what the accessibility tree reports. The panel appears because you removed `hidden`, changed CSS, or inserted the node. The attribute is a parallel description that your code must keep in step with the real visibility, which is exactly why stale values are such a common defect.
  • Should a button that submits a form or navigates to another page carry aria-expanded?
    No. Adding it announces an expand/collapse affordance the control does not have, so a user who activates it expecting content to appear in place gets a page change instead. `aria-expanded` is only for controls that show and hide content, and it is supported on a defined set of roles such as button, link, combobox, tab and treeitem.

saying these in an interview costs you the question

  • Putting aria-expanded on the panel instead of the trigger
  • Removing the attribute rather than setting it to "false"
  • Assuming the attribute's presence alone means expanded
  • Thinking aria-expanded="true" actually reveals the content
  • Adding aria-expanded to buttons that merely navigate

context

open as a page

ARIA has several states that all seem to mean "this one is on" — aria-checked, aria-selected, aria-pressed and aria-current. Which widget does each belong to?

level: middleimportance: should knowfreq 40%

basics

~20 s

aria-checked belongs to checkboxes, radios and switches; aria-selected to items chosen inside a composite such as a listbox option, tab or grid row; aria-pressed to toggle buttons; and aria-current marks the current item in a set, like the active nav link.

open as a page

In ARIA, how do the properties aria-haspopup, aria-controls and aria-owns differ in what they tell assistive technology?

level: middleimportance: should knowfreq 36%

basics

~20 s

aria-haspopup says a control opens a popup and of what type; aria-controls names, by id, the element this control governs; aria-owns re-parents elements in the accessibility tree so they become children of this element despite the DOM saying otherwise.

open as a page

In ARIA, what is the difference between a role, a state, and a property, and how does that difference show up in the markup?

level: middleimportance: should knowfreq 52%

basics

~20 s

An ARIA role says what an element is (role="tab"), a state says what it is doing right now and changes with interaction (aria-expanded), and a property describes a stable trait or relationship (aria-haspopup). States and properties are both aria-* attributes.

open as a page

When should a control use aria-disabled="true" instead of the HTML disabled attribute, and what must you handle yourself if you do?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Use aria-disabled when the control must stay focusable and discoverable — toolbar items, or a submit button whose blocked reason the user needs to read. It only announces the state, so you must block the action, style the control, and keep it out of any effect yourself.

open as a page