react-aria-live: announcing changes to screen readers
@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: 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:
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.
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.
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:
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 and
Announcer — started as
GitHub issues describing observed screen-reader behaviour, not speculative API
surface.
Links
- npm: @umxr/react-aria-live
- Source: github.com/umxr/react-aria-live
Revisions
- Created.
- Published.