On a traditional site every change arrives with a page load and the screen reader starts reading again. In a React 7,897 application nothing loads: results filter, a row is deleted, a save succeeds, and a user not looking at the screen is told none of it. Live regions are the fix — mark a container aria-live and the browser reports its text changes without moving focus. Two roles carry the usual settings: role="status" is polite and atomic, re-reading the region after a pause; role="alert" interrupts.
The React trap is timing. A live region only announces changes made after the browser is already observing it, and {msg && <p role="status">{msg}</p>} creates the region and its text in one commit, which screen readers routinely miss. Render the container from the first render; change only what is inside.
function Search({ cities }) {
const [q, setQ] = useState('');
const hits = cities.filter(c => c.toLowerCase().includes(q.toLowerCase()));
return <div>
<label htmlFor="q">Filter cities</label>
<input id="q" value={q} onChange={e => setQ(e.target.value)} />
{/* present from the first render, so later text changes are announced */}
<p role="status">{q && `${hits.length} of ${cities.length} match`}</p>
<ul>{hits.map(c => <li key={c}>{c}</li>)}</ul>
</div>;
}After typing os, the region carries the settings its role implies, and the new text is what gets spoken:
status live=polite atomic=True relevant=additions text StaticText "2 of 5 match"
Use live regions sparingly. Everything polite queues, so a region updated on every keystroke builds a backlog the user cannot interrupt: debounce the message or derive it from useDeferredValue (useDeferredValue). Keep role="alert" for submission failures, and handle route changes by moving focus (Focus Management). Under the hood the browser diffs the region's subtree against its previous contents and hands the changed text to the platform accessibility API — it never sees React state, only DOM changes.