ARIA (Accessible Rich Internet Applications)

ARIA is a set of attributes that make web content and applications more accessible to people with disabilities, especially those using assistive technologies like screen readers. ARIA supplements HTML to bridge accessibility gaps in complex web applications.

Core Principles

Only use ARIA when necessary

html
<!-- Bad: Unnecessary ARIA -->
<button role="button">Click me</button>

<!-- Good: Native HTML is better -->
<button>Click me</button>

<!-- Good: ARIA when needed -->
<div role="button" tabindex="0">Custom button</div>

Don't change native semantics

html
<!-- Bad: Conflicting role -->
<h1 role="button">I'm confused</h1>

<!-- Good: Use appropriate element -->
<button>I'm a button</button>

Make interactive elements keyboard accessible

html
<!-- Bad: Not keyboard accessible -->
<div role="button" onclick="handleClick()">Click</div>

<!-- Good: Keyboard accessible -->
<div role="button" tabindex="0"
     onclick="handleClick()"
     onkeydown="if(event.key==='Enter'||event.key===' ')handleClick()">
  Click
</div>

ARIA Attributes

Roles

Define what an element is or does.

html
<!-- Landmark roles -->
<header role="banner">Site header</header>
<nav role="navigation">Main navigation</nav>
<main role="main">Main content</main>
<aside role="complementary">Sidebar</aside>
<footer role="contentinfo">Footer</footer>

<!-- Widget roles -->
<div role="button">Custom button</div>
<div role="tab">Tab item</div>
<div role="dialog">Modal dialog</div>
<div role="alert">Error message</div>
<div role="progressbar">Loading...</div>

<!-- Document structure roles -->
<div role="article">Article content</div>
<div role="list">
  <div role="listitem">Item 1</div>
  <div role="listitem">Item 2</div>
</div>

States and Properties

aria-label

Provides an accessible name.

html
<!-- Icon-only button -->
<button aria-label="Close dialog">
  <svg><!-- X icon --></svg>
</button>

<!-- Search input -->
<input type="text" aria-label="Search products" placeholder="Search...">

<!-- Override visible text when needed -->
<button aria-label="Add item to cart">
  <span>Add to cart</span>
  <span class="price">$19.99</span>
</button>

aria-labelledby

References another element as the label.

html
<h2 id="dialog-title">Confirm Delete</h2>
<div role="dialog" aria-labelledby="dialog-title">
  Are you sure you want to delete this item?
</div>

<!-- Multiple labels -->
<div id="billing-title">Billing Address</div>
<div id="shipping-title">Shipping Address</div>
<form aria-labelledby="billing-title shipping-title">
  <!-- Form content -->
</form>

aria-describedby

References additional description.

html
<input
  type="email"
  aria-describedby="email-hint email-error"
  required
>
<p id="email-hint">We'll never share your email</p>
<p id="email-error" class="error">Please enter a valid email</p>

aria-hidden

Hides content from assistive technologies.

html
<!-- Decorative icons -->
<span aria-hidden="true">🎉</span>

<!-- Hidden duplicate content -->
<div aria-hidden="true">
  <!-- Visible but ignored by screen readers -->
</div>

<!-- Warning: Don't hide focusable elements -->
<!-- Bad -->
<button aria-hidden="true">Click me</button>

aria-live

Announces dynamic content changes.

html
<!-- Polite: Wait for user to finish -->
<div aria-live="polite">
  Item added to cart
</div>

<!-- Assertive: Interrupt immediately -->
<div aria-live="assertive" role="alert">
  Error: Payment failed
</div>

<!-- Off: Don't announce (default) -->
<div aria-live="off">Updates</div>

<!-- Common pattern: Status messages -->
<div role="status" aria-live="polite" aria-atomic="true">
  3 items remaining
</div>

aria-expanded

Indicates whether an element is expanded or collapsed.

html
<button
  aria-expanded="false"
  aria-controls="menu"
  onclick="toggleMenu()"
>
  Menu
</button>

<nav id="menu" hidden>
  <!-- Menu items -->
</nav>

<script>
function toggleMenu() {
  const button = document.querySelector('[aria-expanded]');
  const menu = document.getElementById('menu');
  const isExpanded = button.getAttribute('aria-expanded') === 'true';

  button.setAttribute('aria-expanded', !isExpanded);
  menu.hidden = isExpanded;
}
</script>

aria-selected

Indicates selection state in tabs, lists, etc.

html
<div role="tablist">
  <button role="tab" aria-selected="true" aria-controls="panel-1">
    Tab 1
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2">
    Tab 2
  </button>
</div>

<div role="tabpanel" id="panel-1">Panel 1 content</div>
<div role="tabpanel" id="panel-2" hidden>Panel 2 content</div>

aria-disabled vs disabled

html
<!-- Native disabled: Not focusable, not in tab order -->
<button disabled>Cannot interact</button>

<!-- aria-disabled: Still focusable, needs JS to prevent action -->
<button aria-disabled="true" onclick="return false;">
  Disabled but focusable
</button>

aria-current

Indicates the current item in a set.

html
<nav>
  <a href="/" aria-current="page">Home</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>

<!-- Other values -->
<a href="#step-2" aria-current="step">Step 2</a>
<a href="#2024-01-15" aria-current="date">January 15</a>
<a href="#section-3" aria-current="location">Section 3</a>

Common Patterns

Modal Dialog

html
<div role="dialog"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc"
     aria-modal="true">

  <h2 id="dialog-title">Confirm Action</h2>
  <p id="dialog-desc">Are you sure you want to continue?</p>

  <button onclick="confirm()">Confirm</button>
  <button onclick="cancel()">Cancel</button>
</div>

<script>
// Trap focus within dialog
// Close on Escape key
// Return focus when closed
</script>

Dropdown Menu

html
<button
  aria-haspopup="true"
  aria-expanded="false"
  aria-controls="dropdown-menu"
>
  Menu
  <span aria-hidden="true"></span>
</button>

<ul id="dropdown-menu" role="menu" hidden>
  <li role="menuitem">
    <a href="/profile">Profile</a>
  </li>
  <li role="menuitem">
    <a href="/settings">Settings</a>
  </li>
  <li role="menuitem">
    <a href="/logout">Logout</a>
  </li>
</ul>

Tabs

html
<div role="tablist" aria-label="Account settings">
  <button
    role="tab"
    aria-selected="true"
    aria-controls="profile-panel"
    id="profile-tab"
  >
    Profile
  </button>
  <button
    role="tab"
    aria-selected="false"
    aria-controls="security-panel"
    id="security-tab"
    tabindex="-1"
  >
    Security
  </button>
</div>

<div role="tabpanel"
     id="profile-panel"
     aria-labelledby="profile-tab">
  Profile content
</div>

<div role="tabpanel"
     id="security-panel"
     aria-labelledby="security-tab"
     hidden>
  Security content
</div>

Loading State

html
<!-- Button with loading state -->
<button aria-busy="true" aria-disabled="true">
  <span aria-hidden="true"></span>
  <span>Loading...</span>
</button>

<!-- Region with loading state -->
<div role="region" aria-busy="true" aria-label="Product list">
  Loading products...
</div>

Form Validation

html
<label for="email">Email</label>
<input
  type="email"
  id="email"
  aria-required="true"
  aria-invalid="false"
  aria-describedby="email-error"
>
<span id="email-error" role="alert" aria-live="polite">
  <!-- Error message appears here -->
</span>

<script>
function validateEmail(input) {
  const isValid = input.validity.valid;
  input.setAttribute('aria-invalid', !isValid);

  const error = document.getElementById('email-error');
  error.textContent = isValid ? '' : 'Please enter a valid email';
}
</script>

Accordion

html
<div class="accordion">
  <h3>
    <button
      aria-expanded="false"
      aria-controls="section1"
      id="accordion1"
    >
      Section 1
    </button>
  </h3>
  <div id="section1"
       role="region"
       aria-labelledby="accordion1"
       hidden>
    Content for section 1
  </div>
</div>

Progress Bar

html
<!-- Determinate -->
<div role="progressbar"
     aria-valuenow="75"
     aria-valuemin="0"
     aria-valuemax="100"
     aria-label="Upload progress">
  <div style="width: 75%">75%</div>
</div>

<!-- Indeterminate -->
<div role="progressbar"
     aria-label="Loading"
     aria-valuetext="Loading, please wait">
  <div class="spinner"></div>
</div>

React Examples

Accessible Button

jsx
function IconButton({ icon, label, onClick }) {
  return (
    <button
      onClick={onClick}
      aria-label={label}
    >
      <span aria-hidden="true">{icon}</span>
    </button>
  );
}

// Usage
<IconButton
  icon="🗑️"
  label="Delete item"
  onClick={handleDelete}
/>

Toggle Button

jsx
function ToggleButton() {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <button
      role="button"
      aria-pressed={isPressed}
      onClick={() => setIsPressed(!isPressed)}
    >
      {isPressed ? 'On' : 'Off'}
    </button>
  );
}

Live Region

jsx
function Notification({ message }) {
  return (
    <div
      role="status"
      aria-live="polite"
      aria-atomic="true"
    >
      {message}
    </div>
  );
}

Testing ARIA

Manual Testing

  • Use screen reader (NVDA, JAWS, VoiceOver)
  • Test keyboard navigation
  • Use browser DevTools accessibility tab
  • Check contrast ratios

Automated Tools

bash
# Install axe-core
npm install -D @axe-core/cli

# Run accessibility tests
axe https://example.com

# Install pa11y
npm install -D pa11y

# Run tests
pa11y https://example.com

React Testing

javascript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('should not have accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Common Mistakes

html
<!-- Bad: Redundant role -->
<button role="button">Click</button>

<!-- Good: Native button -->
<button>Click</button>

<!-- Bad: Missing keyboard support -->
<div role="button" onclick="handleClick()">Click</div>

<!-- Good: Full keyboard support -->
<div
  role="button"
  tabindex="0"
  onclick="handleClick()"
  onkeydown="if(event.key==='Enter')handleClick()"
>
  Click
</div>

<!-- Bad: aria-label on non-interactive -->
<div aria-label="Important">Content</div>

<!-- Good: Use visible text or aria-labelledby -->
<div>
  <h2 id="title">Important</h2>
  <div aria-labelledby="title">Content</div>
</div>

Best Practices

  • Use native HTML elements when possible
  • Test with actual screen readers
  • Provide text alternatives for all non-text content
  • Ensure keyboard accessibility
  • Use ARIA sparingly and correctly
  • Keep dynamic content announcements concise
  • Test color contrast
  • Provide skip links for navigation
  • Use semantic HTML first, ARIA second

Learn More