Standards · ARIA

State Global state

aria-busy

Marks a region as in the middle of an update so assistive technology suppresses interim announcements. Set to "true" while loading or while large DOM changes are in progress; flip back to "false" when the region is stable.

When to use

When a region is being asynchronously rebuilt — a search result list streaming in, a table re-sorting itself, a chat panel waiting on a network response. Without aria-busy, screen readers connected to a live region may announce every partial state as it appears, which is noisy and often confusing.

The pattern is:

  1. Set aria-busy="true" on the container before you start mutating it.
  2. Do the work (clear children, fetch data, re-render).
  3. Set aria-busy="false" once the DOM is stable.

If the container is also a live region (aria-live="polite" or aria-live="assertive"), the screen reader holds back announcements while aria-busy="true" and produces a single coherent announcement after aria-busy flips back.

Outside a live region the attribute is still useful: it signals to AT that the user should not act on the content yet, and it interacts well with progress indicators.

How to keep it in sync

Valid values are "true" and "false". Default to "false" (or omit).

The attribute must wrap the mutation. The common bug is setting aria-busy="true", mutating the DOM, and forgetting to set it back — the region then appears permanently “loading” to assistive technology even though everything is visually fine.

Pair it with a visible loading indicator (spinner, skeleton, progress text) so sighted users get the same signal. The aria-busy attribute is for AT only; it does not render anything on its own.

Common failures

  • Setting aria-busy="true" and never setting it back to "false".
  • Setting aria-busy after the mutation has already happened — too late, the interim announcements have already fired.
  • Using aria-busy alone with no aria-live and no visible spinner. Sighted users see nothing; AT users hear nothing.
  • Setting aria-busy on the whole <body> for the entire app lifetime. The scope should match the region that is actually changing.
  • Using aria-busy instead of role="progressbar" for a determinate progress indicator. The two are complementary, not alternatives.
  • Forgetting to also disable interactive controls inside the busy region — sighted users still see and can click them.

Example

<section
  id="results"
  aria-live="polite"
  aria-busy="true"
>
  <p>Loading results…</p>
</section>

<script>
  fetch('/api/search')
    .then((r) => r.json())
    .then((data) => {
      const region = document.getElementById('results');
      region.innerHTML = renderResults(data);
      region.setAttribute('aria-busy', 'false');
    });
</script>