Dots

Live Region

Reads short status messages to screen reader users, even while a sheet or dialog is open.

"use client"

import * as React from "react"

Live Region gives you an announce() helper and a LiveRegion component that send messages to one shared, visually hidden announcer at the end of the page. Use it for results that sighted users notice without moving focus, such as "Statement ready", "Card frozen" or "12 transactions shown". Pick announce() for one-off events from a handler, and LiveRegion when the message follows a piece of state. For an error tied to a form field, use the field's own error part instead, since it is read with the field.

Installation

pnpm dlx shadcn@latest add @dots/live-region

Usage

import { announce, LiveRegion } from "@/components/ui/live-region"
<Button onPress={() => announce("Card frozen")}>Freeze card</Button>
 
<LiveRegion>{`${count} transactions shown`}</LiveRegion>

Features

  • One body-level announcer with a polite and an assertive log, created once and reused
  • Stays audible while a React Aria modal hides the rest of the page, through data-react-aria-top-layer
  • Waits briefly after the announcer is created so the first message is not lost
  • Every announce() call adds a new line, so the same message twice is read twice
  • Lines are removed after 7 seconds, so the log never grows
  • LiveRegion renders nothing and announces only when its text changes, not on mount

Announce from a handler

Call announce() after an action finishes, as in the demo at the top of this page. It works from inside a sheet or dialog, where a live region rendered in the page would be hidden from screen readers.

<Button
  isPending={isPending}
  onPress={async () => {
    setIsPending(true)
    announce("Preparing your statement")
    await exportStatement()
    setIsPending(false)
    announce("Statement ready")
  }}
>
  Prepare PDF
</Button>

Component

Render LiveRegion with a single string. It announces the new text each time it changes. The text it mounts with is not read, because it is already on the page.

24 transactions

"use client"

import * as React from "react"
<LiveRegion>{`${count} transactions shown`}</LiveRegion>

Assertive

Pass "assertive" only for errors that block the user, such as a failed payment. It interrupts whatever the screen reader is saying. Show the same message on screen too.

"use client"

import * as React from "react"
announce("Payment failed. Your balance is too low.", "assertive")

Accessibility

The announcer is a visually hidden container with two role="log" regions, one aria-live="polite" and one aria-live="assertive", both with aria-relevant="additions". It is marked as a React Aria top layer, so ariaHideOutside leaves it readable while a sheet, dialog or alert dialog is open.

  • Keep messages short and complete: "Statement ready", not "Done".
  • Use polite for confirmations and progress. Use assertive only for blocking errors.
  • Do not announce what already moves focus. A dialog that opens reads its own title.
  • Do not announce every keystroke. Wait until a value settles, as PercentSplitRemainder does.
  • Pass a single string to LiveRegion, for example a template literal. JSX with several text nodes is not a string.
  • To repeat the same message, call announce() again. LiveRegion skips unchanged text.

API Reference

announce

Reads a message to screen reader users.

announce(message: string, politeness?: LiveRegionPoliteness): void
ParameterTypeDefaultDescription
messagestring-The text to read. An empty string does nothing.
politeness"polite" | "assertive""polite""assertive" interrupts. Use it for errors only.

LiveRegion

Announces its text each time it changes. Renders nothing.

PropTypeDefaultDescription
childrenstring-The message as one string. An empty string announces nothing.
politeness"polite" | "assertive""polite"How the change is announced.

LiveRegionPoliteness

type LiveRegionPoliteness = "polite" | "assertive"