# react-aria-live: announcing changes to screen readers

@umxr/react-aria-live is an open-source React package I built for ARIA live regions: a provider, a useAnnounce hook with priority and queue control, declarative components, and test utilities for asserting what a screen reader hears. It exists because two things reliably go wrong with live regions: identical messages that screen readers silently skip, and announcements that focus-trapped modals swallow. This is its canonical reference page.

Published: 2026-07-29
Canonical: https://umar.codes/react-aria-live

## Revisions

- 2026-07-29 — Created.
- 2026-07-29 — Published.

---

@umxr/react-aria-live is a React package I built for announcing dynamic content
changes to screen readers. It wraps ARIA live regions in a provider, a
`useAnnounce` hook, and declarative components, and ships test utilities so
announcements can be asserted like any other output. Version 1.1.0 is 47.9 kB
unpacked with zero runtime dependencies.

## Why live regions need a library

A visual change — a toast, a cart count, a validation error — is invisible to a
screen reader user unless something announces it. The platform primitive for
this is an [ARIA live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions):
an element whose content changes are read aloud. The primitive is simple; using
it correctly is not. Announcements need somewhere stable to live in the DOM,
messages sent in quick succession need queueing, and two screen-reader
behaviours — ignoring repeated messages and ignoring regions outside a focus
trap — need deliberate workarounds. This package is those workarounds written down
once, with tests.

## The API in brief

Wrap the app once, announce from anywhere:

```tsx
import { LiveRegionProvider, useAnnounce } from "@umxr/react-aria-live";

function SaveButton() {
  const announce = useAnnounce();

  const handleSave = async () => {
    await saveData();
    announce("Document saved");
  };

  return <button onClick={handleSave}>Save</button>;
}
```

`announce()` takes options for `priority` (`polite` by default, `assertive` to
interrupt), `delay`, `clearQueue`, and `clearAfter`. Alongside the hook there
are declarative components: `LiveRegion` for content that should announce when
it re-renders, `Announce` for one-off messages, and pre-configured `Alert`,
`Status`, and `Log` wrappers with the matching ARIA roles. The provider renders
the underlying live-region elements once at the body level, so announcements
have a stable home regardless of which component fired them.

## Screen readers skip repeated messages

A live region announces on *content change*. Announce "Cart note saved", then
announce the exact same string later, and the second one is silence — the DOM
content didn't change, so there's nothing to read. This is the kind of bug
nobody catches in a visual review.

The fix is the `clearAfter` option, added in 1.1.0: the region empties itself
the given number of milliseconds after announcing, so the next identical
message registers as a fresh change.

```tsx
announce("Cart note saved", { clearAfter: 500 });
```

## Announcements inside focus traps

Live regions at the body level have a failure mode: when focus is trapped
inside a modal, screen readers may never read regions outside it. Your
announcement fires, the DOM updates, and the user hears nothing.

The `Announcer` component exists for this. Render one inside the modal and it
subscribes to the same announcement queue as every other announcer, so a single
`useAnnounce()` call reaches all of them — and the screen reader picks it up
from whichever region sits in the active focus context.

```tsx
function Modal({ children }) {
  return (
    <div role="dialog" aria-modal="true">
      <Announcer />
      {children}
    </div>
  );
}
```

## Testing what a screen reader hears

Announcements are output, and output should be assertable. The
`@umxr/react-aria-live/test` subpath exports `getAnnouncements()`,
`getLastAnnouncement()`, `hasAnnouncement()`, and `clearMockAnnouncements()`,
so a test can check that clicking save actually announced something — without
spinning up a screen reader:

```tsx
await userEvent.click(screen.getByRole("button"));
expect(getAnnouncements()).toContain("Document saved");
```

The package's own suite is 76 tests across 11 files, against roughly 2,000
lines of source.

## Design constraints

The package is deliberately small: zero runtime dependencies, React 18 or 19 as
the only peer, MIT licensed, TypeScript-first with types shipped in the 47.9 kB
published output. Both shipped features beyond the core —
[`clearAfter`](https://github.com/umxr/react-aria-live/issues/3) and
[`Announcer`](https://github.com/umxr/react-aria-live/issues/2) — started as
GitHub issues describing observed screen-reader behaviour, not speculative API
surface.

## Links

- npm: [@umxr/react-aria-live](https://www.npmjs.com/package/@umxr/react-aria-live)
- Source: [github.com/umxr/react-aria-live](https://github.com/umxr/react-aria-live)
