# announce-queue > Screen-reader announcements for React, played one at a time. A provider renders two visually > hidden `aria-live` regions; a hook pushes messages through a queue that paces them, drops stale > ones, and lets urgent messages interrupt. - Package: `announce-queue` (npm), MIT, ESM + CJS, TypeScript types included. - Peer dependency: `react >= 18`. No runtime dependencies. - Repository: https://github.com/kirilinsky/announce-queue - Live demo: https://kirilinsky.github.io/announce-queue/ ## When to use it Use it when a React app changes something a sighted user notices visually but a screen-reader user would not: a saved draft, a filtered table, a failed upload, an async result, a toast. Announcing means putting text into an `aria-live` region — this package owns that region and the timing. Do not use it for content that is already in the accessible tree and focusable (a dialog, an inline error tied to an input via `aria-describedby`); announce only what has no other accessible signal. ## Install ```sh npm install announce-queue ``` ## Minimal usage ```tsx // app root, once import { AnnounceQueueProvider } from 'announce-queue' export default function RootLayout({ children }: { children: React.ReactNode }) { return {children} } ``` ```tsx // anywhere below the provider import { useAnnounce } from 'announce-queue' const { announce, clear } = useAnnounce() announce('Draft saved') // polite: waits its turn announce('Connection lost', { priority: 'assertive' }) // interrupts polite speech clear() // drop everything pending, e.g. on a route change ``` ## API ```ts function AnnounceQueueProvider(props: { children: ReactNode cooldown?: number // ms of silence after a node is removed. Default 150 dedupe?: boolean // skip a message identical to one still pending. Default true clearAfter?: number | 'auto' // ms the node stays in the DOM. Default 'auto' (from text length) maxQueue?: number // pending messages kept per priority, oldest dropped first. Default 3 onEvent?: (event: AnnounceEvent) => void // lifecycle observer for devtools/demos }): JSX.Element function useAnnounce(): { announce: (message: string, options?: AnnounceOptions) => void clear: (priority?: 'assertive' | 'polite') => void } // options override provider defaults per call type AnnounceOptions = { priority?: 'assertive' | 'polite' // default 'polite' cooldown?: number dedupe?: boolean clearAfter?: number | 'auto' } type AnnounceEvent = { type: 'enqueue' | 'skip' | 'drop' | 'insert' | 'clear' id: number message: string priority: 'assertive' | 'polite' cooldown: number clearAfter: number interrupted?: boolean // on 'clear': removed early by an alert or by clear() } // framework-free engine, for non-React hosts function createAnnounceQueue(config?: AnnounceQueueConfig): { announce(message: string, options?: AnnounceOptions): void clear(priority?: 'assertive' | 'polite'): void attach(assertiveEl: HTMLElement, politeEl: HTMLElement): void destroy(): void } function estimateReadingTime(message: string): number // ms, 1000–6000 ``` ## Behaviour rules - One message per priority is audible at a time: `clearAfter + cooldown` passes between insertions. - Assertive is taken first, removes any live polite node immediately, and holds the polite lane until the alert is gone. - Dedupe applies to the pending queue only, never to history. - Overflow past `maxQueue` drops the oldest pending message, because a late announcement is worse than none. - Each announcement is a brand-new DOM node (never a `textContent` mutation), the regions carry no `aria-atomic`, and a repeat of the previous text gets an invisible trailing space — the three things VoiceOver and NVDA need in order not to stay silent. - The provider writes nothing to the DOM before its mount effect, so it is safe to render on the server. Messages announced before mount are buffered. - `useAnnounce()` outside the provider throws. ## Common recipes ```tsx // route change: stop talking about the screen the user left useEffect(() => clear, [pathname, clear]) // form submit try { await save() announce('Changes saved') } catch { announce('Could not save changes', { priority: 'assertive' }) } // long text that should be read in full announce(summary, { clearAfter: 'auto' }) // default; do not hand-tune unless you must ```