ARIA States and Properties
HTML elements have built-in meaning, and the browser keeps track of their state for you. A checkbox knows whether it is checked. A details knows whether it is open. A button that is disabled is understood as unavailable. Whenever possible, use these.
But when you build something custom, the browser cannot know what you mean. That is the job of ARIA states and properties: small attributes that tell assistive technology what is happening to a thing, and how things relate to one another. They are simple to write, and easy to get out of date, which is where most mistakes come from.
What You Will Learnβ
- The difference between ARIA states and properties
- The kinds of values ARIA attributes take
- The most useful ARIA attributes:
aria-expanded,aria-pressed,aria-selected,aria-current,aria-busy, and more - Why you must keep states in sync with what people see
- How native states and ARIA states interact
- How to check your ARIA attributes
The Big Ideaβ
- HTML Code
- State Sync Lab
<!-- A disclosure: the state says open or closed -->
<button type="button" aria-expanded="false" aria-controls="details">
Delivery details
</button>
<div id="details" hidden>Free delivery on orders over 20 dollars.</div>
<!-- A toggle: the state says on or off -->
<button type="button" aria-pressed="false">Favourite</button>
<!-- Tabs: the state says which one is chosen -->
<div role="tablist" aria-label="Recipe">
<button role="tab" aria-selected="true" aria-controls="p1" id="t1">Ingredients</button>
<button role="tab" aria-selected="false" aria-controls="p2" id="t2" tabindex="-1">Method</button>
</div>
<script>
button.addEventListener("click", () => {
const open = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!open)); // keep it in sync!
panel.hidden = open;
});
</script>
Disclosure
Told to assistive technology: Delivery details, button, collapsed
Toggle button
Told to assistive technology: Favourite, toggle button, not pressed
Tabs (use the left and right arrow keys)
Told to assistive technology: Ingredients, tab, selected
Everything agrees.
Try each widget with the bug off, and then on. The picture changes, but with the bug on, the state attributes stay frozen. A person using a screen reader hears the wrong state, and cannot tell that anything happened. When the state is correct, the attribute is the only way they know. (The lines show what the attributes say. The exact words a screen reader uses differ.)
Use all three widgets with the bug off. Then tick the box, and use them again. The page looks the same, but a screen reader user is now told the wrong thing.
States and Propertiesβ
ARIA splits its attributes into two groups. In practice the line is fuzzy, and you rarely need to care which is which. The idea behind it is useful, though:
| States | Properties | |
|---|---|---|
| What they describe | Something that changes as people use the page | Something that is fixed, or changes rarely |
| Who changes them | Your script, often | Usually you, once, in the HTML |
| Examples | aria-expanded, aria-pressed, aria-checked, aria-selected, aria-current, aria-disabled, aria-invalid, aria-busy, aria-hidden | aria-label, aria-labelledby, aria-describedby, aria-controls, aria-haspopup, aria-required, aria-orientation, aria-level |
You can think of a property as "what it is like," and a state as "what it is doing right now."
What Values Look Likeβ
ARIA attributes take a few kinds of values. Getting the kind right matters, because browsers ignore a value they do not understand.
| Kind | What it looks like | Examples |
|---|---|---|
| True or false | The words true or false, in quotes | aria-disabled="true" |
| Tri-state | true, false, or mixed | aria-checked="mixed" |
| Token | One word from a fixed list | aria-live="polite", aria-current="page" |
| ID reference | The id of another element | aria-labelledby="title" |
| ID reference list | Several ids, with spaces | aria-describedby="hint error" |
| String | Plain text, in the page language | aria-label="Close" |
| Number or integer | A number | aria-valuenow="40", aria-level="2" |
A few more notes:
- Write
trueandfalseas words, notyes,on, or1. - Write values in lowercase, to be safe.
- A missing attribute and
falseare not always the same. Foraria-pressed, a missing attribute means "this is not a toggle," andfalsemeans "a toggle that is off." Follow the rules for each one. - IDs must exist, and be unique on the page.
Try some values in the tester below, including wrong ones.
- HTML Code
- Value Tester
<!-- Right -->
<button aria-expanded="false">Menu</button>
<button aria-pressed="mixed">Select all</button>
<a href="/recipes" aria-current="page">Recipes</a>
<input aria-describedby="hint error">
<!-- Wrong: ignored, or unpredictable -->
<button aria-expanded="yes">Menu</button>
<button aria-pressed="on">Mute</button>
<div role="heading" aria-level="two">Title</div>
Your attribute: aria-expanded="true"
Valid.
Type of value: true / false / undefined
Used on: A button or other control that opens and closes something
Put it on the control, not on the panel. Remove it only when the control does not expand at all.
<button aria-expanded="false" aria-controls="menu">Menu</button>Try the wrong things on purpose: aria-expanded="yes", aria-pressed="on", aria-level="two". Browsers do not complain about an invalid value. They just ignore it, or treat it unpredictably, which is why a checker is useful. (This list is a selection, not every ARIA attribute.)
The Big Trap: Keeping State in Syncβ
An ARIA state is a promise about the page right now. If the panel is open and the attribute says false, ARIA is lying to the person who relies on it. Unlike a visual bug, nobody using a screen reader can see the mismatch to report it. They simply get confused.
Some ways to avoid it:
- Update the state in the same place you update the page. The line that opens the panel is also the line that sets
aria-expanded. - Use one source of truth. In a framework, bind the attribute to your state variable (
aria-expanded={open}), so they can never disagree. - Style from the attribute. If your CSS uses
[aria-expanded="true"]to show the open look, then a forgotten attribute is also a visible bug, and gets fixed quickly:
button[aria-expanded="true"] .chevron {
transform: rotate(180deg);
}
[role="tab"][aria-selected="true"] {
border-bottom: 3px solid #1d4ed8;
}
- Test every state change with a screen reader, or the Accessibility panel, and not only by looking.
- Set the starting state in the HTML, so the page is right before any script runs.
Put It on the Right Elementβ
A state belongs on the element a person interacts with, or that has that role:
| Attribute | Put it on | Not on |
|---|---|---|
aria-expanded | The button that opens the panel | The panel |
aria-selected | The tab | The tabpanel |
aria-pressed | The toggle button | A wrapper div |
aria-current | The link or step that is current | The whole list |
aria-invalid | The form field | The error message |
aria-busy | The region being updated | The button that started it |
The States You Will Use Mostβ
aria-expandedβ
For anything that opens and closes: a menu button, an accordion header, a disclosure. It goes on the trigger. (If you can, use the native details and summary, which need none of this.)
aria-pressedβ
For a toggle button that is on or off (Mute, Bold, Favourite). Keep the label the same, and let the state do the changing. Use mixed for a "select all" button when only some items are selected.
aria-checkedβ
For custom checkboxes, radios, and switches. For a native checkbox, do not add it, because the browser already tracks checked, and the two can disagree.
aria-selectedβ
For the chosen item in a set of tabs, options, or rows. Exactly one tab in a tab list should be selected at a time.
aria-currentβ
For where you are: page for the current page link in a menu, step for the current step in a process, location, date, and time for other cases, and true if none fit. Do not rely on a colour or a bold style alone, because a screen reader cannot see them.
aria-disabledβ
Says a control is unavailable, but leaves it focusable. You must block the action yourself, and make it look unavailable. For plain form controls, the native disabled attribute is simpler. See Accessible Buttons for when to choose which.
aria-invalidβ
Marks a field with a problem. Set it to true when the field has an error, together with a visible error message linked by aria-describedby. Remove it (or set it to false) once the problem is fixed. See Accessible Forms.
aria-busyβ
Says a region is being updated, so a screen reader can wait until it finishes, rather than reading half-loaded content. Support is uneven, so also give a visible message, and a role="status" announcement, as in the lab below.
aria-hiddenβ
Removes something from assistive technology completely. It has its own lesson, because it is easy to misuse: aria-hidden.
Properties You Will Use Oftenβ
aria-label,aria-labelledby,aria-describedby: names and descriptions. Each has its own lesson soon.aria-controls: says which element a control changes. Support in screen readers is limited, so treat it as a helper. The real work is done by good structure, and byaria-expanded.aria-haspopup: says a control opens a menu, a list, or a dialog. Support varies, so use it witharia-expanded, not instead of it.aria-required: for custom widgets. For native fields, userequired.aria-orientation: says whether a toolbar, tab list, or slider ishorizontalorvertical.aria-level: the heading level of a custom heading, or the depth of a tree item.aria-valuenow,aria-valuemin,aria-valuemax,aria-valuetext: for sliders and progress bars. Usearia-valuetextwhen the number needs a unit or a word ("3 stars," "Medium").
Native and ARIA: Do Not Fightβ
When both a native feature and an ARIA attribute describe the same thing, the native one is the truth. A checkbox with checked and aria-checked="false" is confusing, and browsers do not agree on which wins. A button with disabled and aria-disabled="false" is just as odd.
- Use the native attribute (
checked,disabled,required,hidden,open) whenever one exists. - Add the ARIA state only where HTML has none (
aria-expandedon a custom button,aria-selectedon a tab,aria-current). - Never set both, and never let them disagree.
Try Current and Busyβ
The lab below shows aria-current in a menu and in a step-by-step process, and aria-busy on a list that loads.
aria-current in a menu
The attribute on the current link: aria-current="page" on "Recipes"
aria-current in a process
- Basket
- Delivery (you are here)
- Payment
- Done
The attribute on the current step: aria-current="step" on "Delivery"
aria-busy while loading
The list says: aria-busy="false"
Notice that the "(you are here)" text and the bold style are for sighted users, and the attribute is what tells a screen reader. Both are needed, and they must always agree. The busy list is hidden from none of the tools, but a screen reader may wait to read it until aria-busy goes back to false. Support for aria-busy varies, so also give a visible and spoken message, as here with role="status".
Check Your States and Propertiesβ
- Open the Accessibility panel in your browser's DevTools, and look at the state of each widget. Click through it, and watch the values change.
- Use the keyboard, and check that every state change happens when it should, including Esc and arrow keys.
- Listen with a screen reader. Does it say "expanded" and "collapsed," "pressed" and "not pressed," "selected," and "current page"?
- Check the starting state with scripts turned off.
- Run an automated checker (axe, Lighthouse) to catch invalid values and ids that point nowhere.
- Search your code for
aria-and ask, for each: does the state change with the page? Is there a native attribute that does it?
Good Habitsβ
- Use native states first (
checked,disabled,required,open). - Keep ARIA states in sync with what people see, from one source of truth.
- Put each attribute on the right element.
- Write values correctly: lowercase
trueandfalse, real tokens, real ids. - Set the starting state in the HTML.
- Style from the attribute, so mistakes show.
- Keep labels stable when a state changes.
- Back every ARIA state with a visible cue, and every visible cue with a state.
- Test with a screen reader.
Accessibility Checklistβ
- Every toggle, disclosure, tab, and custom checkbox has the right state attribute.
- States change at the same moment as the page does.
- Starting states are correct in the HTML.
- State attributes are on the correct elements.
- Values are valid, in lowercase, with real tokens.
- Every id in
aria-controls,aria-labelledby, andaria-describedbyexists. - No native control also has a disagreeing ARIA state.
-
aria-currentmarks the current page or step. - Errors use
aria-invalidand a visible message, and clear when fixed. - Loading regions give a visible and spoken message as well as
aria-busy.
Security and Privacy Notesβ
- State attributes are public. Anyone can read
aria-*values in the page source, and assistive technology reads them aloud. Do not store secrets, personal data, or tokens in them. - Do not expose private information through state. An
aria-invalid="true"on a login field, or a "valid" and "invalid" announcement on a password check, could reveal something about an account, so keep messages general ("Email or password is incorrect"). - States must tell the truth. A state that says "selected" or "checked" when it is not, or a loading state used to hide content, is deceptive and can lead people to take actions by mistake.
- Be careful with consent controls. A toggle for tracking or marketing must report its real state, so that people using assistive technology know what they have agreed to.
- Do not use state to profile people. How someone uses states and focus can reveal that they use assistive technology, and so reveal a disability.
- Keep ids unpredictable where it matters. Reusing the same ids for sensitive elements across pages makes automation (and scraping) easier, though this is a minor concern next to correct markup.
Global Audience Notesβ
- Translate string values (
aria-label,aria-valuetext,aria-description) with the rest of your page. States such astrueandfalseare code, and are never translated. - Format
aria-valuetextfor the reader. Numbers, units, and dates differ by region ("3,5 stars" or "3.5 stars"), so build the text with a locale-aware formatter. - Right-to-left languages:
aria-orientationand arrow-key behaviour in tabs and sliders should follow the reading direction. Test them in a right-to-left layout. - Screen readers announce states in the user's own language, such as "expanded," "pressed," and "selected." Do not write these words into your labels, because the user would hear them twice.
- Slow connections: a loading state (
aria-busywith a visible message) is more important when pages take a while to update. Say what is happening, in short, translatable words. - Different tools differ. Support for states such as
aria-busy,aria-haspopup, andaria-controlsvaries. Test on the devices and screen readers your audience uses.
Common Mistakes to Avoidβ
- Never updating the state after the page changes.
aria-expandedon the panel, not the button.- Invalid values such as
aria-expanded="yes". aria-checkedon a native checkbox.- Both
disabledandaria-disabled. aria-selectedon every tab, or on none.- Changing the label and the state together, such as "Unmute" with
aria-pressed. - Pointing
aria-controlsat an id that does not exist. - Relying on
aria-busyalone to tell people something is loading. - Using
aria-currentwithout any visible cue, or the other way round. - Writing the state words in the label ("Menu, expanded").
- Never checking the Accessibility panel.
Practice Timeβ
Try these in your browser and your editor:
- Use the state sync lab with the bug off and on. Write down what is different for a screen reader user.
- Use the value tester with five wrong values, and explain why each is wrong.
- Build a disclosure button with
aria-expanded, and watch it in the Accessibility panel. - Build a toggle with
aria-pressed. Keep the label fixed. - Style your tabs from
[aria-selected="true"], and delete the JavaScript that updates it. What breaks? - Add
aria-current="page"to your navigation, and style it. - Add a step list with
aria-current="step"to a checkout page. - Mark a field
aria-invalid, with an error linked byaria-describedby. - Add a loading state with
aria-busyand arole="status"message. - Bonus: take a custom checkbox made from a
div, and replace it with a native one. What ARIA could you delete?
Quick Recapβ
Click a question to check your answer.
1. What is the difference between a state and a property?
A state describes something that changes as people use the page (aria-expanded, aria-pressed). A property describes something fixed or rarely changing (aria-label, aria-controls).
2. Why must you keep ARIA states in sync with the page?
The state is the only way a screen reader user knows what changed. If it is out of date, they are told something false.
3. Which element gets aria-expanded?
The control that opens and closes the content (usually a button), not the content panel.
4. When should you not use aria-checked?
On a native checkbox or radio button. The browser already tracks checked, and the two can disagree.
5. What does aria-current="page" do?
It marks the link to the page you are on, so a screen reader can say "current page." It needs a visible cue as well.
6. What happens if you write an invalid value, like aria-expanded="yes"?
Browsers do not warn you. They ignore the value, or treat it unpredictably, so the state is lost.
Key Takeawaysβ
- States describe what is happening now. Properties describe what it is like and how things relate.
- Use native states first, and ARIA only to fill gaps.
- Keep every state in sync with the page, from one source of truth.
- Put each attribute on the right element: the trigger, the tab, the field.
- Use valid values: lowercase
trueandfalse, real tokens, real ids. - Pair every ARIA state with a visible cue, and every visible cue with an ARIA state.
- Check in the Accessibility panel, and test with a screen reader.
Next, learn how to give names to controls and images with aria-label.