announce-queue
v0.2.0
Published
Screen-reader announcements for React — an aria-live queue that paces messages, drops stale ones, and lets alerts interrupt
Maintainers
Readme
announce-queue
Screen-reader announcements for React, played one at a time.
npm install announce-queueimport { AnnounceQueueProvider, useAnnounce } from 'announce-queue'
// once, at the root
;<AnnounceQueueProvider>{children}</AnnounceQueueProvider>
// anywhere below it
const { announce, clear } = useAnnounce()
announce('Draft saved') // waits its turn
announce('Connection lost', { priority: 'assertive' }) // interrupts
clear() // drop everything, e.g. on a route changeThat is the whole surface. react >= 18 is the only peer dependency; there are no runtime ones.
What it does for you
A message holds its live region for as long as it takes to read, then the node is removed and the next one goes in — polite updates never talk over each other. Alerts skip the queue and cut off whatever polite message is being read. Bursts do not pile up: only the newest few messages survive, because an announcement read fifteen seconds late is worse than none.
It also handles the three screen-reader quirks that make hand-rolled live regions go silent: every
message is a brand-new DOM node, the regions carry no aria-atomic, and a repeat of the previous
text gets an invisible trailing space.
API
<AnnounceQueueProvider>
Mount it once, at the root. It renders the two visually hidden regions and owns the queue.
| Prop | Type | Default | Description |
| ------------ | -------------------------------- | -------- | ----------------------------------------------------------------------------- |
| clearAfter | number \| 'auto' | 'auto' | ms the node stays in the DOM. 'auto' derives it from the text (1000–6000ms) |
| cooldown | number | 150 | ms of silence after a node is removed, before the next one of that priority |
| maxQueue | number | 3 | pending messages kept per priority; the oldest are dropped first |
| dedupe | boolean | true | drop a message identical to one still pending in the same priority |
| onEvent | (event: AnnounceEvent) => void | — | observes every enqueue, skip, drop, insert and clear |
Nothing touches the DOM before the mount effect, so it is safe to render on the server. Messages announced before mount are buffered.
useAnnounce()
Returns { announce, clear }. Throws if called outside the provider. Per-call options override the
provider defaults:
announce('Upload failed', {
priority: 'assertive', // default 'polite'
clearAfter: 2000, // or 'auto'
cooldown: 300,
dedupe: false,
})clear(priority?) drops what is pending and empties the regions — pass a priority to leave the
other one alone:
const { clear } = useAnnounce()
const pathname = usePathname()
useEffect(() => clear, [pathname, clear]) // stop talking about the screen the user leftAlso exported
createAnnounceQueue(config) — the same engine without React: announce, clear,
attach(assertiveEl, politeEl), destroy(). estimateReadingTime(message) — the number behind
clearAfter: 'auto'.
Rules the queue follows
- One message per priority is audible at a time:
clearAfter + cooldownbetween insertions. - Assertive is taken first, removes any live polite node at once (its
clearevent carriesinterrupted: true), and holds the polite lane until the alert is gone. - Dedupe applies to the pending queue only, never to history.
- Overflow past
maxQueuedrops the oldest pending message and reports it asdrop.
Development
npm install
npm run demo # the playground at demo/, wired to src/ rather than a build
npm run ci # format, types, knip, tests, both buildsContributions welcome — see CONTRIBUTING.md.
For coding agents: llms.txt carries the same API in a machine-readable form, ships
inside the package, and is served at
/llms.txt.
License
MIT
