Roles, states, and properties
ARIA attributes split into three categories. Once you can name them, the spec stops feeling like a wall of jargon.
ARIA has roughly seventy attributes. They look chaotic until you notice they fall into three buckets: roles, states, and properties. Once you know which bucket an attribute lives in, you know roughly how it behaves and when to update it. This lesson sorts them out, then drills into the most-confused pair in the whole spec — aria-disabled versus the plain HTML disabled attribute.
Roles: what a thing is
A role answers the question "what is this element?" Buttons, links, headings, dialogs, tabs, alerts — all roles. You set a role with the role attribute, and you set it once. Roles don't usually change at runtime; a tab doesn't suddenly become a checkbox.
Native HTML elements come with built-in roles. <button> has the button role; <nav> has the navigation role; <input type="checkbox"> has the checkbox role. You only add role= when there's no native element that fits, or when the native one doesn't quite match the pattern you're building.
<!-- Built-in roles, no ARIA needed --> <button>Save</button> <nav>...</nav> <dialog open>...</dialog> <!-- Explicit role for a custom widget --> <div role="tablist"> <div role="tab" aria-selected="true">Overview</div> <div role="tab" aria-selected="false">Activity</div> </div>
A handful of roles are called landmark roles — banner, main, navigation, complementary, contentinfo, search. Screen readers let users jump between landmarks the way sighted users skim by section. Most of them have native equivalents (<header>, <main>, <nav>, <aside>, <footer>); reach for the HTML element first.
States: what a thing is doing
A state is something that changes. A checkbox can be checked or unchecked. A disclosure can be expanded or collapsed. A button can be pressed or not (think of a "Mute" toggle). State attributes start with aria- and almost always end up updated by JavaScript as the user interacts.
Common states:
aria-checked— checkboxes, radios, switches. Values:true/false/mixed.aria-expanded— disclosures, accordions, comboboxes. Values:true/false.aria-pressed— toggle buttons (mute, bold, pin). Values:true/false.aria-selected— tabs, options in a listbox. Values:true/false.aria-current— the current item in a set (the active page in a nav). Values:page,step,true,false.aria-disabled— the control is disabled. Values:true/false.
If you forget to update a state when the underlying UI changes, the screen reader keeps announcing the old value. That's worse than not having the state at all, because the user is now actively misled.
Rule of thumb: roles are set in HTML and rarely change. States are set in HTML or JavaScript and change as the user interacts. If you find yourself writing element.setAttribute("role", ...) at runtime, that's almost always wrong.
Properties: what a thing has
A property is metadata about an element that's less likely to change than a state. Names, descriptions, relationships, value ranges. Examples:
aria-label,aria-labelledby,aria-describedby— names and descriptions (lesson 3).aria-controls— "this control affects that other element."aria-haspopup— "activating me opens a popup of this type."aria-valuemin,aria-valuemax,aria-valuenow,aria-valuetext— for sliders and meters.aria-required— "this field must be filled in."aria-readonly— "you can read this but not change it."
Properties can change, but they tend to describe the thing structurally. The line between "state" and "property" is fuzzy in places, and the spec itself sometimes hedges. You don't need to memorize the line — just remember that an attribute starting with aria- is one of the two, and that it modifies what assistive tech reports.
aria-disabled vs the disabled attribute
This is the single most-asked ARIA question, so it gets its own section. The two attributes look similar. They behave very differently.
The native disabled attribute (on <button>, <input>, <select>, <textarea>, <fieldset>) does four things:
- Greys out the control visually (browser default styles).
- Removes it from the tab order — keyboard users skip past it.
- Prevents click and key events from firing.
- Excludes the field from form submission.
aria-disabled="true" does none of those. It only changes what the screen reader announces — "Submit, button, dimmed" or "unavailable", depending on the screen reader. The control is still focusable, still clickable, still in form data. You have to enforce the rest yourself.
<!-- Native disabled: skipped by Tab, click ignored, not submitted --> <button disabled>Submit</button> <!-- aria-disabled: still focusable, still clickable — only the announcement changes --> <button aria-disabled="true" onclick="handleClick">Submit</button>
So why does aria-disabled exist? Because sometimes you want the user to be able to focus a disabled-looking control — maybe to read its tooltip explaining why it's disabled. A truly disabled button is unreachable; if the user can't reach it, they can't discover the explanation. With aria-disabled, you keep the control in the tab order but prevent its action in JavaScript.
If you use aria-disabled, you must also: (1) intercept the click in JavaScript and do nothing, (2) restyle the control yourself so it looks disabled, and (3) remember it'll still submit unless you guard against it server-side too.
Try it yourself
aria-disabled="true" on a Save button. The user clicks it. What happens by default?