slop audit
Blog post 53 / Accessibility

aria-live: announce status updates and test them

Use aria-live and role=status for accessible status updates, fix silent or repeated announcements, and test browser behavior before your website launches.

By Slop Audit · 8 min read

aria-live identifies content updates that assistive technology should announce without moving keyboard focus. Use it for short feedback such as a search result count or a completed save. Choose the announcement's urgency, keep its container stable, and test the actual screen-reader experience before launch. A message appearing on screen does not establish that someone heard it.

This guide covers dynamic announcements. Use the website form testing checklist for validation and request outcomes, and the accessible name guide for labeling the controls that trigger them.

aria-live launch checklist

  1. List the status changes on the page's main task path.
  2. Choose polite feedback for ordinary updates and reserve interruption for urgent messages.
  3. Render the announcement container before changing its text.
  4. Keep the message concise and understandable without nearby visual context.
  5. Check that retries, rerenders, and rapid updates do not produce confusing feedback.
  6. Assert markup, visible results, and focus behavior in browser tests.
  7. Verify announcements with the browser and screen-reader combinations you support.

Choose aria-live polite, assertive, or a status role

The MDN aria-live reference defines three values. polite allows an update to wait for a suitable pause. assertive requests immediate attention and can interrupt speech. off does not request ordinary live announcements, although changes can still be announced when focus is within the region.

Start with the least disruptive choice that communicates the event:

  • Search completed or settings saved: use role="status" for a brief result.
  • A custom region with ordinary updates: use aria-live="polite".
  • An urgent failure requiring immediate awareness: consider role="alert".
  • An expanding menu or a changed input value: expose the control's state with its proper native or ARIA semantics; do not add an announcement to every interaction.

W3C's Status Messages explanation distinguishes feedback about results, waiting, progress, and errors from other dynamic content. A search result count is status feedback; the entire results list is not. Dialogs that take focus and expanded controls are different cases. Adding a live region to the whole page can make ordinary navigation unnecessarily noisy.

Do not use an alert simply because the message is red. Consider what the person needs to do immediately and whether interrupting their current reading helps. Keep useful error text on screen so users can return to it.

Create the region before updating its content

For an ordinary result count, start with a stable, empty container:

<form id="project-search">
  <label for="query">Find a project</label>
  <input id="query" name="q" type="search" required>
  <button type="submit">Search projects</button>
</form>
<p id="search-status" role="status" aria-atomic="true"></p>
<ul id="search-results"></ul>

W3C's ARIA22 technique explains that status implies polite live behavior and atomic updates. Explicit aria-atomic="true" helps communicate the full message in environments that do not apply that default consistently. Establish the role before the status change occurs.

Update the existing paragraph when the response arrives:

const status = document.querySelector("#search-status");

function showResultCount(count) {
  status.textContent = count === 1
    ? "1 project found."
    : `${count} projects found.`;
}

This fragment assumes the HTML above is already mounted and the caller supplies the result count. A production search also needs result rendering, failure handling, and protection against stale responses. Keep those responsibilities separate from the short announcement.

In React or another component framework, keep the region mounted outside conditional loading branches. Update its text rather than replacing the container with a new keyed element. Avoid inserting a fully populated ordinary live region and expecting reliable speech on its first appearance. MDN's live-region guide recommends establishing the region before the change; alert behavior has special cases and should not be used as a workaround for routine feedback.

Give each announcement enough context

“12” is ambiguous when the count appears far from the current reading position. “12 projects found” identifies the result. “Saved” may also need context when several editable panels share a page: “Profile saved” is clearer.

Keep the live container small. With atomic updates, including the full results list or a large settings panel can cause excessive content to be repeated. Put the short summary in the status region and leave ordinary content available for navigation elsewhere.

Review these product states explicitly:

  • Zero results: say that the search finished with no matches.
  • Failure: explain that the result is unavailable and where to retry.
  • Repeated success: make sure another save remains understandable even when its visible wording is unchanged.
  • Rapid requests: avoid announcing results from an older request after a newer one completes.

Do not append timestamps or invisible random text just to force announcements. If repeating identical text produces no speech in a supported combination, test a deliberate clear-then-update strategy with a distinct update boundary. There is no universal timeout that guarantees speech across assistive technologies. Preserve a readable outcome even if an announcement is delayed or missed.

For type-ahead search, avoid sending an announcement for every keystroke. Debounce work where appropriate, cancel or ignore obsolete requests, and announce the settled result. If the page includes progress, report meaningful stages rather than every percentage increment.

Keep focus, errors, and busy state separate

An announcement does not need to receive keyboard focus. Leave focus on the search control after an ordinary result update so the user can continue their task. For dialogs and route changes, use the appropriate focus-management pattern instead; see the keyboard accessibility testing guide.

For invalid fields, preserve their visible labels, expose invalid state when applicable, and associate the explanation with the affected input. An alert alone does not tell a returning user which field needs correction. An error summary that deliberately receives focus may already communicate the failure; check whether adding an alert creates redundant speech.

Avoid stacking role="alert" and aria-live="assertive" without a tested reason. The MDN live-region guide notes that this combination can produce duplicate announcements in VoiceOver on iOS. Also inspect nested live regions and third-party toast libraries when one event speaks twice.

aria-busy serves another purpose: it indicates that updates are incomplete. The MDN aria-busy reference describes how assistive technology can defer changes until the region is ready. If used, reset it to false on success and failure. Placing your “Searching” message inside a region that stays busy can prevent it from being announced when expected. Keep the brief status outside a separately busy results container when that matches the interaction.

Add an aria-live browser regression check

A browser test can protect the announcement container and task behavior. This Playwright example assumes your public preview implements the HTML contract above and fetches results from /api/projects?q=.... Adapt the route, response shape, and selectors to your application:

import { test, expect } from "@playwright/test";

test.use({ serviceWorkers: "block" });

test("search keeps a stable status region and focus", async ({ page }) => {
  const target = process.env.PREVIEW_URL;
  if (!target) throw new Error("Set PREVIEW_URL to your deployed preview");

  await page.route("**/api/projects?*", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({ projects: [{ id: "portfolio", name: "Portfolio" }] }),
    });
  });

  await page.goto(target);
  const status = page.locator("#search-status");
  await expect(status).toHaveAttribute("role", "status");
  await expect(status).toHaveAttribute("aria-atomic", "true");
  await expect(status).toBeEmpty();
  const originalRegion = await status.elementHandle();
  if (!originalRegion) throw new Error("Missing status container");

  await page.getByRole("searchbox", { name: "Find a project" }).fill("Portfolio");
  const submit = page.getByRole("button", { name: "Search projects" });
  await submit.focus();
  await submit.press("Enter");

  await expect(page.getByRole("status")).toHaveText("1 project found.");
  await expect(status).toBeVisible();
  await expect(page.locator("#search-results")).toContainText("Portfolio");
  await expect(submit).toBeFocused();
  expect(await originalRegion.evaluate((node) =>
    node === document.querySelector("#search-status")
  )).toBe(true);
});

Playwright documents mocking API responses and retrying locator assertions. The mocked response makes this a frontend contract check; it does not verify the deployed API. Add zero-result, failure, and repeated-search cases to your website regression testing routine.

Passing this test proves neither speech delivery nor announcement order. It checks that the expected message, stable node, visible result, and focus survive the interaction. Run a separate integration check with the real API.

Verify screen-reader announcements before launch

On the deployed preview, enable a supported screen reader and follow the task with the keyboard. Record the browser, operating system, screen reader and versions, tested build, action, visible outcome, and what was announced.

Test a successful search, zero results, failure, rapid consecutive searches, and the same search twice. Listen while other content is being read: does ordinary feedback wait appropriately, and does an urgent message interrupt only when necessary? Confirm that focus remains useful and the result can still be found after speech ends.

When an update is silent, inspect whether the region existed beforehand, whether it or an ancestor is hidden, whether busy state remained active, and whether the framework actually changed the text. When feedback repeats, check for nested announcers, duplicate component mounts, and both a toast and a separate announcement service handling the same event.

Keep observed results with the release evidence. A DOM inspection cannot substitute for this pass.

Check the surrounding page with Slop Audit

After testing aria-live behavior, run Slop Audit on the public release URL. When Browser Run completes, its report includes axe findings, form-label and accessible-name observations, sampled keyboard-focus evidence, and desktop/mobile screenshots. Use those findings to address nearby barriers that could prevent someone from reaching or understanding the status update.

Slop Audit does not run a screen reader, listen to announcements, or verify the search scenarios above. Its report complements your live-region tests. Fix the observed markup or task issue, repeat the manual announcement checks, and rescan the public page after the change.