Skip to main content

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​

index.html
<!-- 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>

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:

StatesProperties
What they describeSomething that changes as people use the pageSomething that is fixed, or changes rarely
Who changes themYour script, oftenUsually you, once, in the HTML
Examplesaria-expanded, aria-pressed, aria-checked, aria-selected, aria-current, aria-disabled, aria-invalid, aria-busy, aria-hiddenaria-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.

KindWhat it looks likeExamples
True or falseThe words true or false, in quotesaria-disabled="true"
Tri-statetrue, false, or mixedaria-checked="mixed"
TokenOne word from a fixed listaria-live="polite", aria-current="page"
ID referenceThe id of another elementaria-labelledby="title"
ID reference listSeveral ids, with spacesaria-describedby="hint error"
StringPlain text, in the page languagearia-label="Close"
Number or integerA numberaria-valuenow="40", aria-level="2"

A few more notes:

  • Write true and false as words, not yes, on, or 1.
  • Write values in lowercase, to be safe.
  • A missing attribute and false are not always the same. For aria-pressed, a missing attribute means "this is not a toggle," and false means "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.

index.html
<!-- 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>

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:

  1. 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.
  2. Use one source of truth. In a framework, bind the attribute to your state variable (aria-expanded={open}), so they can never disagree.
  3. 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;
}
  1. Test every state change with a screen reader, or the Accessibility panel, and not only by looking.
  2. 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:

AttributePut it onNot on
aria-expandedThe button that opens the panelThe panel
aria-selectedThe tabThe tabpanel
aria-pressedThe toggle buttonA wrapper div
aria-currentThe link or step that is currentThe whole list
aria-invalidThe form fieldThe error message
aria-busyThe region being updatedThe 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 by aria-expanded.
  • aria-haspopup: says a control opens a menu, a list, or a dialog. Support varies, so use it with aria-expanded, not instead of it.
  • aria-required: for custom widgets. For native fields, use required.
  • aria-orientation: says whether a toolbar, tab list, or slider is horizontal or vertical.
  • 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. Use aria-valuetext when 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-expanded on a custom button, aria-selected on 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.

http://127.0.0.1:5500/index.html

aria-current in a menu

The attribute on the current link: aria-current="page" on "Recipes"

aria-current in a process

  1. Basket
  2. Delivery (you are here)
  3. Payment
  4. 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​

    1. 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.
    2. Use the keyboard, and check that every state change happens when it should, including Esc and arrow keys.
    3. Listen with a screen reader. Does it say "expanded" and "collapsed," "pressed" and "not pressed," "selected," and "current page"?
    4. Check the starting state with scripts turned off.
    5. Run an automated checker (axe, Lighthouse) to catch invalid values and ids that point nowhere.
    6. 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​

    1. Use native states first (checked, disabled, required, open).
    2. Keep ARIA states in sync with what people see, from one source of truth.
    3. Put each attribute on the right element.
    4. Write values correctly: lowercase true and false, real tokens, real ids.
    5. Set the starting state in the HTML.
    6. Style from the attribute, so mistakes show.
    7. Keep labels stable when a state changes.
    8. Back every ARIA state with a visible cue, and every visible cue with a state.
    9. 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, and aria-describedby exists.
    • No native control also has a disagreeing ARIA state.
    • aria-current marks the current page or step.
    • Errors use aria-invalid and 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 as true and false are code, and are never translated.
    • Format aria-valuetext for 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-orientation and 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-busy with 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, and aria-controls varies. Test on the devices and screen readers your audience uses.

    Common Mistakes to Avoid​

    1. Never updating the state after the page changes.
    2. aria-expanded on the panel, not the button.
    3. Invalid values such as aria-expanded="yes".
    4. aria-checked on a native checkbox.
    5. Both disabled and aria-disabled.
    6. aria-selected on every tab, or on none.
    7. Changing the label and the state together, such as "Unmute" with aria-pressed.
    8. Pointing aria-controls at an id that does not exist.
    9. Relying on aria-busy alone to tell people something is loading.
    10. Using aria-current without any visible cue, or the other way round.
    11. Writing the state words in the label ("Menu, expanded").
    12. Never checking the Accessibility panel.

    Practice Time​

    Try these in your browser and your editor:

    1. Use the state sync lab with the bug off and on. Write down what is different for a screen reader user.
    2. Use the value tester with five wrong values, and explain why each is wrong.
    3. Build a disclosure button with aria-expanded, and watch it in the Accessibility panel.
    4. Build a toggle with aria-pressed. Keep the label fixed.
    5. Style your tabs from [aria-selected="true"], and delete the JavaScript that updates it. What breaks?
    6. Add aria-current="page" to your navigation, and style it.
    7. Add a step list with aria-current="step" to a checkout page.
    8. Mark a field aria-invalid, with an error linked by aria-describedby.
    9. Add a loading state with aria-busy and a role="status" message.
    10. 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 true and false, 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.