Screen Readers

ARIA Live Regions

12 min read

ARIA Live Regions

Live regions allow screen readers to announce dynamic content changes without the user needing to navigate to that content. They're essential for notifications, alerts, loading states, and any content that updates asynchronously.

How Live Regions Work

When content inside a live region changes, the screen reader announces the new content. The announcement behavior is controlled by the aria-live attribute.

The aria-live Attribute

html
<!-- Polite: Waits for pause in user activity -->
<div aria-live="polite">Status updates appear here</div>

<!-- Assertive: Interrupts user immediately -->
<div aria-live="assertive">Critical alerts here</div>

<!-- Off: No announcements (default) -->
<div aria-live="off">Silent updates</div>

When to Use Each

aria-live="polite"

Use for most updates that don't require immediate attention:

  • Form submission confirmations
  • Status messages
  • Chat messages
  • Search results count
  • Loading indicators
html
<div aria-live="polite" class="status-message">
  <!-- Content updated dynamically -->
  Your changes have been saved.
</div>

aria-live="assertive"

Reserve for urgent messages requiring immediate attention:

  • Error messages
  • Session timeouts
  • Critical alerts
  • Validation failures
html
<div aria-live="assertive" class="error-message">
  <!-- Critical errors -->
  Connection lost. Please check your internet connection.
</div>

Implicit Live Region Roles

Some ARIA roles have built-in live region behavior:

html
<!-- role="alert" = aria-live="assertive" -->
<div role="alert">Error: Form submission failed</div>

<!-- role="status" = aria-live="polite" -->
<div role="status">Saving...</div>

<!-- role="log" = aria-live="polite" -->
<div role="log">Chat messages appear here</div>

<!-- role="marquee" = aria-live="off" -->
<div role="marquee">Scrolling text</div>

<!-- role="timer" = aria-live="off" -->
<div role="timer">00:05:32</div>

aria-atomic

Controls whether the entire region or just changes are announced:

html
<!-- Announces entire region on any change -->
<div aria-live="polite" aria-atomic="true">
  <span class="count">5</span> items in cart
</div>
<!-- Announces: "5 items in cart" -->

<!-- Announces only the changed element -->
<div aria-live="polite" aria-atomic="false">
  <span class="count">5</span> items in cart
</div>
<!-- Announces: "5" -->

aria-relevant

Specifies which changes trigger announcements:

html
<!-- Announce when content is added (default) -->
<div aria-live="polite" aria-relevant="additions">

<!-- Announce removals -->
<div aria-live="polite" aria-relevant="removals">

<!-- Announce both additions and removals -->
<div aria-live="polite" aria-relevant="additions removals">

<!-- Announce all changes including text -->
<div aria-live="polite" aria-relevant="all">

aria-busy

Indicates content is being updated (delays announcements):

html
<div aria-live="polite" aria-busy="true">
  Loading content...
</div>

<!-- After loading completes -->
<div aria-live="polite" aria-busy="false">
  Content loaded successfully
</div>

Practical Examples

Toast Notifications

jsx
function Toast({ message, type }) {
  return (
    <div
      role={type === 'error' ? 'alert' : 'status'}
      className={`toast toast-${type}`}
    >
      {message}
    </div>
  );
}

Form Validation

jsx
function Form() {
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  return (
    <form>
      {/* Assertive for errors */}
      <div aria-live="assertive" className="error-region">
        {error && <p className="error">{error}</p>}
      </div>

      {/* Polite for success */}
      <div aria-live="polite" className="success-region">
        {success && <p className="success">Form submitted successfully!</p>}
      </div>

      {/* Form fields */}
    </form>
  );
}

Loading States

jsx
function DataList() {
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState([]);

  return (
    <div
      aria-live="polite"
      aria-busy={loading}
      aria-atomic="true"
    >
      {loading ? (
        <p>Loading data...</p>
      ) : (
        <>
          <p>{data.length} items found</p>
          <ul>
            {data.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </>
      )}
    </div>
  );
}

Search Results Count

jsx
function SearchResults({ query, results }) {
  return (
    <div>
      <div role="status" aria-atomic="true">
        {results.length} results found for "{query}"
      </div>
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

Best Practices

  1. Don't overuse live regions: Too many announcements overwhelm users
  1. Be concise: Short, clear messages work best
  1. Test with screen readers: Different screen readers handle live regions differently
  1. Consider timing: Content must exist before being announced
  1. Prefer role over aria-live: role="status" is clearer than aria-live="polite"

Common Mistakes

Mistake 1: Live region on frequently updating content

html
<!-- Bad: Announces every second! -->
<div aria-live="polite">
  <span id="timer">05:32</span>
</div>

<!-- Good: Use role="timer" which is off by default -->
<div role="timer">
  <span id="timer">05:32</span>
</div>

Mistake 2: Adding live region and content simultaneously

jsx
// Bad: Live region must exist before content changes
{showAlert && (
  <div role="alert">{alertMessage}</div>
)}

// Good: Live region always exists
<div role="alert">
  {showAlert && alertMessage}
</div>

Live Region Checklist

  • aria-live="polite" for most updates
  • aria-live="assertive" only for critical messages
  • Live region exists in DOM before content updates
  • Messages are concise and meaningful
  • Not overused (prevents announcement fatigue)
  • Tested with multiple screen readers

Was this article helpful?