ARIA Roles, States, and Properties

▶ Try It Yourself

ARIA Roles, States, and Properties

1. Introduction

ARIA — Accessible Rich Internet Applications — is a W3C specification that supplements native HTML semantics, particularly for custom interactive components where no suitable native element exists. ARIA attributes fall into three categories: roles (what an element is), states (its current condition), and properties (its characteristics). Used correctly, ARIA bridges the gap between visual UI and the accessibility tree. Used incorrectly, it actively harms assistive technology users.

2. Concept

ARIA Attribute Categories

Category Examples Changes With Interaction?
Roles role=”dialog”, role=”alert”, role=”tab”, role=”button” No — set once
States aria-expanded, aria-checked, aria-selected, aria-disabled Yes — updated by JavaScript
Properties aria-label, aria-labelledby, aria-describedby, aria-controls Rarely — mostly static
Note: The first rule of ARIA: if a native HTML element provides the semantics and behaviour you need, use it instead of ARIA. A <button> is better than <div role="button" tabindex="0"> in almost every case — it requires less code and works without JavaScript.
Tip: aria-label overrides all other naming sources. aria-labelledby references another element’s text as the accessible name. Prefer aria-labelledby when a visible label already exists — it creates a single source of truth for sighted and non-sighted users.
Warning: Never apply an ARIA role without implementing the required keyboard interaction pattern. A role="tab" must respond to arrow keys. A role="dialog" must trap focus. Failing to do so creates a broken experience that is worse than no ARIA at all.

3. Basic Example

<!DOCTYPE html>
<html lang="en">
  <head><meta charset="UTF-8"><title>ARIA Demo</title></head>
  <body>

    <!-- aria-label: accessible name for icon-only button -->
    <button type="button" aria-label="Close dialog">
      <svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
        <line x1="2" y1="2" x2="14" y2="14" stroke="currentColor" stroke-width="2"/>
        <line x1="14" y1="2" x2="2" y2="14" stroke="currentColor" stroke-width="2"/>
      </svg>
    </button>

    <!-- aria-expanded: toggle button state -->
    <button type="button" aria-expanded="false" aria-controls="panel-1">
      Show Details
    </button>
    <div id="panel-1" hidden>
      <p>Expanded panel content.</p>
    </div>

    <!-- aria-live: announce dynamic content changes -->
    <div role="status" aria-live="polite" id="cart-count">
      3 items in cart
    </div>

    <!-- aria-describedby: link hint text to input -->
    <label for="password">Password</label>
    <input type="password" id="password" name="password" aria-describedby="pw-hint">
    <p id="pw-hint">Minimum 8 characters, must include a number.</p>

    <!-- aria-labelledby: use visible heading as accessible name -->
    <section aria-labelledby="skills-h">
      <h2 id="skills-h">Technical Skills</h2>
      <p>HTML, CSS, JavaScript, React</p>
    </section>

  </body>
</html>

4. How It Works

Step 1 — aria-label for Icon Buttons

An icon-only button has no visible text. Without an accessible name, a screen reader announces “button” with no indication of purpose. aria-label="Close dialog" provides the accessible name. aria-hidden="true" on the SVG prevents its path data or title from being announced separately.

Step 2 — aria-expanded and aria-controls

When a button toggles a panel, aria-expanded must reflect current state: "false" when collapsed, "true" when open. aria-controls identifies the controlled element, helping AT users understand the relationship. Always update both via JavaScript on each click.

Step 3 — aria-live Regions

Content injected into the DOM after page load is not announced by screen readers by default. A aria-live="polite" region queues announcements for when the user is idle. Use aria-live="assertive" only for critical errors — it interrupts the current announcement immediately.

Step 4 — aria-describedby vs aria-labelledby

aria-labelledby sets the primary accessible name from another element. aria-describedby adds supplementary description announced after the name and role. Both accept space-separated lists of ids to reference multiple elements in order.

5. Real-World Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Accordion FAQ</title>
    <style>
      .panel[hidden] { display: none; }
      button[aria-expanded="true"] .icon { transform: rotate(180deg); }
      .icon { display: inline-block; transition: transform 0.2s; }
      :focus-visible { outline: 3px solid #0070f3; outline-offset: 2px; }
    </style>
  </head>
  <body>
    <h1>Frequently Asked Questions</h1>

    <h2>
      <button type="button" aria-expanded="false" aria-controls="faq-1" id="faq-btn-1">
        What is your returns policy?
        <span class="icon" aria-hidden="true">▼</span>
      </button>
    </h2>
    <div id="faq-1" class="panel" role="region" aria-labelledby="faq-btn-1" hidden>
      <p>You can return any item within 30 days for a full refund, provided it is unused and in original packaging.</p>
    </div>

    <h2>
      <button type="button" aria-expanded="false" aria-controls="faq-2" id="faq-btn-2">
        How long does delivery take?
        <span class="icon" aria-hidden="true">▼</span>
      </button>
    </h2>
    <div id="faq-2" class="panel" role="region" aria-labelledby="faq-btn-2" hidden>
      <p>Standard delivery: 3–5 business days. Express next-day delivery available for orders before 2 PM.</p>
    </div>

    <script>
      document.querySelectorAll('[aria-controls]').forEach(function(btn) {
        btn.addEventListener('click', function() {
          var open = btn.getAttribute('aria-expanded') === 'true';
          btn.setAttribute('aria-expanded', String(!open));
          document.getElementById(btn.getAttribute('aria-controls')).hidden = open;
        });
      });
    </script>
  </body>
</html>

6. Common Mistakes

ARIA role without keyboard interaction

<div role="button" onclick="doAction()">Click Me</div>

Use native <button> — keyboard support built in

<button type="button" onclick="doAction()">Click Me</button>

Stale aria-expanded not updated after toggle

<button aria-expanded="false" onclick="panel.hidden = false">Show</button>

Always sync aria-expanded with actual panel state

<button aria-expanded="false" id="tog">Show</button>
<script>
  var t = document.getElementById('tog');
  t.addEventListener('click', function() {
    t.setAttribute('aria-expanded', t.getAttribute('aria-expanded') === 'false' ? 'true' : 'false');
  });
</script>

7. Try It Yourself

▶ Try It Yourself

8. Quick Reference

Attribute Type Common Use
aria-label Property Name icon buttons, inputs without visible labels
aria-labelledby Property Link visible heading as accessible name
aria-describedby Property Attach hint or error text to inputs
aria-expanded State Toggle open/closed of controlled element
aria-controls Property Identify element controlled by a button
aria-live Property Announce dynamic DOM changes (polite/assertive)
aria-hidden State Hide decorative elements from assistive technology

9. Quiz

🧠 Test Yourself

Which ARIA attribute must be updated by JavaScript each time a toggle button opens or closes its panel?





▶ Try It Yourself