In ARIA, what is the difference between a role, a state, and a property, and how does that difference show up in the markup?
answer
- three kinds, one attribute namespace
- what it is, what it's doing, what it relates to
- volatility is the dividing line
- ten states, everything else a property
- a role is a contract with required states
basics
~20 sAn 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.
solid answer
~40 sARIA splits its vocabulary three ways. The `role` attribute declares **what a thing is** — `button`, `tab`, `listbox`, `dialog` — and overrides the element's implicit role. **States** describe a condition that changes during the session, usually because the user acted: ARIA defines exactly ten, including `aria-expanded`, `aria-checked`, `aria-selected`, `aria-disabled`, `aria-invalid`, `aria-busy` and `aria-hidden`. **Properties** are everything else — `aria-haspopup`, `aria-controls`, `aria-owns`, `aria-label`, `aria-required`, `aria-level` — describing stable characteristics and relationships fixed when the widget is built. In the markup there is no syntactic difference: states and properties are both `aria-*` attributes with string values, so the split is conceptual, about volatility. What matters practically is that a role is a contract — `role="checkbox"` requires `aria-checked`, `role="slider"` requires `aria-valuenow` — and the states you promise must actually be kept up to date.
go deeper
Be able to point at markup and say which attribute is the role, which is the state, and which is the property, and remember that every ARIA value is a quoted string, not a present-or-absent HTML boolean.
Explain that states are the volatile values your code must keep synchronised while properties are authored once, and name the common members of each group without hesitating.
Show that you treat a declared role as a contract: name its required states, say who owns keeping them accurate, and describe how you catch a widget whose accessibility tree has drifted from its visible state.
Own the position that wrong ARIA is worse than no ARIA, and be ready to argue where a codebase should draw the line between native elements and hand-built roles given the ongoing maintenance cost of the state contract.
## What ARIA is for HTML elements already carry semantics: a `<button>` is exposed to assistive technology as a button, a `<nav>` as a navigation landmark, a checked `<input type="checkbox">` as a checked checkbox. ARIA (Accessible Rich Internet Applications) is a vocabulary for saying those same things about markup that does not carry them natively, and for expressing widget conditions HTML has no attribute for at all. Crucially, ARIA changes **only what is exposed to the accessibility tree** — it adds no behaviour, no keyboard handling, no styling. The vocabulary comes in three kinds. ## Roles: what the thing is The `role` attribute declares an element's role, replacing its implicit one. The specification groups roles into families: - **Widget roles** — the interactive leaves: `button`, `checkbox`, `switch`, `slider`, `tab`, `menuitem`, `option`, `treeitem`. - **Composite widget roles** — containers that hold and coordinate other widgets: `tablist`, `listbox`, `menu`, `menubar`, `tree`, `grid`, `radiogroup`, `combobox`. - **Document structure roles** — `list`, `listitem`, `heading`, `table`, `row`, `figure`, `separator`, `group`. - **Landmark roles** — `navigation`, `main`, `banner`, `complementary`, `contentinfo`, `search`, `form`, `region`. - **Window roles** — `dialog`, `alertdialog`. - **Live region roles** — `alert`, `status`, `log`, `timer`. There are also **abstract roles** (`widget`, `input`, `structure`, `composite`) that exist only to organise the taxonomy; they must never appear in markup. The attribute technically accepts a space-separated fallback list, where the first token the browser recognises wins, but in practice you write a single value. ## States: the changing condition ARIA 1.2 defines exactly ten states: `aria-busy`, `aria-checked`, `aria-current`, `aria-disabled`, `aria-expanded`, `aria-grabbed` (deprecated), `aria-hidden`, `aria-invalid`, `aria-pressed`, `aria-selected`. A state answers "what is true about this element *right now*" — is the menu open, is the box ticked, is this field in error. States are expected to flip during the user's session, and your code is responsible for flipping them the moment the underlying condition changes. ## Properties: stable traits and relationships Everything else in the `aria-*` namespace is a property: `aria-haspopup`, `aria-controls`, `aria-owns`, `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-required`, `aria-readonly`, `aria-level`, `aria-posinset`, `aria-setsize`, `aria-orientation`, `aria-valuemin`/`aria-valuemax`. A property describes something about the element's nature or its relationship to other elements, normally decided when the widget is authored. The split is a guideline, not a hard rule enforced by anything. `aria-valuenow` is classified as a property yet changes on every drag of a slider; `aria-label` is a property that a component may well rewrite. The useful takeaway is the *intent*: states are the volatile ones you must remember to synchronise. ## The markup does not distinguish them ```html <button aria-expanded="false" aria-haspopup="menu" aria-controls="acct-menu">Account</button> <ul id="acct-menu" role="menu" hidden> <li role="menuitem">Profile</li> <li role="menuitem">Sign out</li> </ul> ``` `aria-expanded` is a state and `aria-haspopup`/`aria-controls` are properties, but they are written identically. Every ARIA value is a **string**: `aria-expanded="false"` is the boolean false, whereas the HTML `hidden` attribute is present-or-absent. Confusing the two conventions is a classic bug — `aria-hidden="false"` does not hide anything, and removing the attribute is not the same as setting it to `"false"` for states like `aria-expanded`, where absence means "not expandable at all". Also note that unknown `aria-*` attributes and unknown role tokens are ignored silently. A typo such as `aria-expandeed` produces no console error and no effect; it simply is not there. ## Why the distinction earns interview time Because a role is a **contract** that names required states and properties. `role="checkbox"` requires `aria-checked`. `role="slider"` requires `aria-valuenow`. `role="combobox"` requires `aria-expanded` and a pointer to the popup it controls. `role="tab"` expects `aria-selected` and lives inside a `role="tablist"`. Declaring the role without maintaining the states produces the worst outcome available: an accessibility tree that confidently reports something false — a menu button stuck at `aria-expanded="false"` while the menu is on screen — which is more damaging than plain, unlabelled markup, because the user has no reason to doubt it.
- If states and properties are written identically in HTML, why does the specification bother separating them?The split signals responsibility. States are the values expected to change as the user interacts, so they carry an implicit maintenance obligation — your component must update them the instant the condition changes. Properties are authored once and largely left alone. Accessibility APIs also expose the two through slightly different channels, but for authors the practical value is knowing which attributes will go stale if nobody keeps them in sync.
- What happens if you declare role="checkbox" but never set aria-checked?The element is announced as a checkbox with no checked state, so a screen reader user is told a control exists but not whether it is on or off. `aria-checked` is a required state for that role, and required means the role's contract is unmet — validators such as axe report it as a violation. The widget looks fine visually and is unusable non-visually.
- Are there roles you should never put in markup?Yes — the abstract roles (`widget`, `input`, `structure`, `composite`, `section`, `sectionhead`, `range`, and similar). They exist purely to organise the role taxonomy in the specification and describe categories, not concrete things. Browsers ignore them, and using one means the element ends up with no useful role at all rather than the one you intended.
saying these in an interview costs you the question
- Thinking aria-hidden="false" hides an element from screen readers
- Believing states and properties use different attribute syntax
- Treating ARIA values as HTML boolean attributes rather than strings
- Adding a role and never updating its required states
- Using abstract roles like role="widget" in real markup