ARIA Properties and States — Complete Guide
In this tutorial, you will learn about ARIA Properties and States. We cover key concepts, practical examples, and best practices to help you master this topic.
ARIA properties and states convey dynamic information about UI components to assistive technologies, such as whether a dropdown is expanded, a checkbox is checked, or an error is present.
What You'll Learn
- The difference between ARIA properties and states
- Commonly used properties like aria-label, aria-describedby, aria-controls
- Commonly used states like aria-expanded, aria-checked, aria-selected
- How to dynamically update ARIA attributes
- Relationship attributes and live region properties
Why It Matters
- Properties and states provide the dynamic context that makes widgets usable
- Missing or incorrect ARIA attributes break screen reader announcements
- Dynamic updates require JavaScript to keep ARIA in sync with visual state
- WCAG requires proper state announcements for custom widgets
Real-World Use
- A dropdown menu uses aria-expanded to indicate open/closed state
- A form field with an error uses aria-invalid and aria-describedby
- A custom checkbox uses aria-checked and handles mixed states
- A tab panel uses aria-controls to link tabs to their panels
flowchart LR A[ARIA Properties] --> B[Static Information] A --> C[Relationships] D[ARIA States] --> E[Dynamic Information] D --> F[User Interaction] A --> G[label, describedby, controls] D --> H[expanded, checked, selected]
Properties vs States
The distinction between properties and states is subtle but important.
Properties are attributes that are less likely to change once set. They describe characteristics of an element:
aria-label— provides an accessible namearia-describedby— provides an accessible descriptionaria-controls— indicates which element this element controlsaria-owns— indicates a parent-child relationship not reflected in the DOMaria-flowto— suggests an alternative reading order
States are attributes that change frequently in response to user interaction or system events:
aria-expanded— whether a collapsible element is expandedaria-checked— whether a checkbox or switch is checkedaria-selected— whether an option is selectedaria-pressed— whether a toggle button is pressedaria-current— the current item in a collection (page, step, location)aria-hidden— whether the element is hidden from assistive technologies
Code Example: Form Input with Error States
<form onsubmit="return validateForm(event)">
<div style="margin-bottom:1rem;">
<label for="email">Email Address</label>
<input type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid="false"
aria-describedby="email-hint email-error">
<div id="email-hint" style="font-size:0.85rem; color:#666;">
Enter your work email address.
</div>
<div id="email-error"
role="alert"
style="color:#cc0000; font-size:0.85rem;"
hidden>
Please enter a valid email address.
</div>
</div>
<button type="submit">Subscribe</button>
</form>
<script>
function validateForm(event) {
event.preventDefault();
const email = document.getElementById('email');
const error = document.getElementById('email-error');
if (!email.value.includes('@') || !email.value.includes('.')) {
email.setAttribute('aria-invalid', 'true');
error.hidden = false;
error.textContent = 'Please enter a valid email address.';
email.focus();
return false;
}
email.setAttribute('aria-invalid', 'false');
error.hidden = true;
alert('Subscribed successfully!');
return false;
}
</script>
Expected output: Screen readers announce "Email Address, edit, required, enter your work email address". When validation fails, the announcement changes to "Email Address, edit, invalid, please enter a valid email address."
Code Example: Menu with Expanded State
<nav aria-label="Account menu">
<button aria-haspopup="true"
aria-expanded="false"
aria-controls="account-menu"
id="account-btn"
onclick="toggleMenu()">
My Account
</button>
<ul id="account-menu"
role="menu"
aria-labelledby="account-btn"
hidden
style="list-style:none; border:1px solid #ddd; padding:0.5rem; background:white;">
<li role="menuitem" tabindex="-1">Profile</li>
<li role="menuitem" tabindex="-1">Orders</li>
<li role="menuitem" tabindex="-1">Settings</li>
<li role="menuitem" tabindex="-1">Sign Out</li>
</ul>
</nav>
<script>
function toggleMenu() {
const button = document.getElementById('account-btn');
const menu = document.getElementById('account-menu');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
if (!isExpanded) {
menu.querySelector('[role="menuitem"]').focus();
} else {
button.focus();
}
}
</script>
Expected output: Screen reader announces "My Account, menu popup, collapsed". Clicking changes it to "expanded". The aria-controls attribute links the button to the menu. Focus moves to the first menu item when opened.
Code Example: Progress Bar with Value Properties
<div id="progress-container">
<div role="progressbar"
id="upload-progress"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
aria-label="File upload progress"
style="width:300px; height:20px; background:#eee; border-radius:10px; overflow:hidden;">
<div id="progress-bar"
style="width:0%; height:100%; background:#4CAF50; transition:width 0.3s;"></div>
</div>
<span id="progress-text" style="margin-left:0.5rem;">0%</span>
</div>
<button onclick="startUpload()">Upload File</button>
<script>
function startUpload() {
const progress = document.getElementById('upload-progress');
const bar = document.getElementById('progress-bar');
const text = document.getElementById('progress-text');
let value = 0;
const interval = setInterval(() => {
value += 5;
progress.setAttribute('aria-valuenow', value);
bar.style.width = value + '%';
text.textContent = value + '%';
if (value >= 100) {
clearInterval(interval);
progress.setAttribute('aria-valuenow', '100');
text.textContent = 'Complete!';
}
}, 200);
}
</script>
Expected output: As the upload progresses, screen readers announce "File upload progress, 50 percent" and continue updating. The aria-valuenow attribute ensures the current value is always communicated.
Common Mistakes
- Setting ARIA attributes once and never updating — If
aria-expandedstays "false" after a menu opens, the screen reader announces incorrect information. - Using aria-label to repeat visible text — If a button already says "Close" visually,
aria-label="Close"is redundant. Use aria-label only when the visible text is insufficient. - Confusing aria-labelledby with aria-describedby — aria-labelledby provides the element's accessible name (replacing visual text). aria-describedby provides additional description.
- Forgetting aria-required on custom form controls — Native form elements have implicit required states. Custom controls need
aria-required="true". - Using aria-hidden inconsistently — If an element is visually hidden but
aria-hidden="false", screen readers announce content the user cannot see. - Not testing state announcements — You cannot assume the correct state will be announced. Test with actual screen readers to verify.
- Overusing aria-live regions — Every live region increases cognitive load for screen reader users. Use them only for truly dynamic content.
Practice Questions
- What is the difference between an ARIA property and an ARIA state? Properties are static attributes that describe characteristics. States change frequently with user interaction or system events.
- What does
aria-expandedindicate and on which elements should it be used? It indicates whether a collapsible element is expanded or collapsed. Use it on buttons or elements that control the visibility of other content. - What is the relationship between
aria-controlsand the id of the controlled element? aria-controls references the id attribute of the element being controlled, creating an explicit relationship for screen readers. - When would you use
aria-valuenow,aria-valuemin, andaria-valuemax? On progress indicators like progress bars, sliders, and spinners to communicate the current value and range. - Challenge: Build an accessible custom slider (range input) using role="slider" with aria-valuenow, aria-valuemin, aria-valuemax, and aria-orientation. Implement keyboard support (Arrow keys increment/decrement, Home/End for min/max) and test with a screen reader.
FAQ
{{< faq "How do I handle mixed checkbox states (partially checked)?" "Use aria-checked=\"mixed\" for tri-state checkboxes that represent a group selection where some items are selected and others are not." >}}Mini Project
Build an accessible custom color picker widget. Use role="radiogroup" for the color options, role="radio" for each color swatch with aria-checked for the selected state, aria-label to announce the color name, and a live region to announce the selected color. Include a text input that updates when a color is selected (two-way binding). Ensure keyboard navigation with Arrow keys. Test with a screen reader.
What's Next
Continue with Lesson 9: Semantic HTML to learn how proper HTML structure creates Accessibility without ARIA.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro