Search topics
AI chat replies read out in pieces as they stream in
An AI assistant writes its reply into a role="log" a few words at a time, so a screen reader announces each piece as it lands, never the answer as a whole.
reply.append(chunk); The problem
Screen reader users hear an AI assistant's answer as a run of broken phrases while it streams, never as one answer they can follow.
Fernhill, an outdoor gear shop, has a help page with an AI assistant on it: a heading, Ask our assistant, a conversation that opens with a greeting, and a question field with a Send button. The conversation is a role="log", so that new messages are read out without anyone leaving the field, as a chat's should be. A screen reader user types "Can I return a tent I've used?" and presses Enter.
The reply streams in, as AI replies do: a few words appear, then a few more, until two sentences fill the bubble. A sighted user reads along as it grows. The screen reader user hears their own question read back, then "Assistant:", then "Yes, within", "30 days", "even if you", "have used it", each said as a separate message as it lands. The answer they asked for arrives as a run of scraps, and when the stream stops they still don't know whether they heard all of it, so they leave the field and read the reply again from the start.
A role="log" is a live region: its additions are announced, politely, in the order they arrive. That is right for a chat where each message arrives whole. Here the script adds each chunk of the stream to the reply as it comes, and every chunk is an addition, so every chunk becomes a message. How a screen reader paces such a run differs between screen readers and browsers. A bug report on one open-source AI chat, tested with NVDA and Firefox, puts it plainly: "every single token or 'chunk' appended to the DOM triggers a new announcement".
Success criterion 4.1.3 asks that status messages can be presented to people by assistive technology without receiving focus, and its Understanding page says the point is to make people aware of the change "in a way that doesn't unnecessarily interrupt their work". The same page describes a related case: where only part of a message is coded as the change, people may hear only that part, "which may not be sufficient information to provide context for the user". The reply here can be found, but what the live region presents is fragments, not the message.
Streaming is designed for people watching the text: it shows that the assistant is working and lets them start reading at once. The longer the reply, the more pieces it is heard in, and a reply with a list or a few steps in it can be read out as dozens of separate scraps.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (affected)
- Seizures (not affected)
- Without vision Screen reader users ask a question and hear the answer in scraps of two or three words, each read as a message of its own as it arrives. They can't tell where one sentence ends, so they wait for the stream to finish and then read the whole reply again to understand it.
- Limited language, cognitive and learning abilities People who listen to pages with a screen reader because reading is hard for them have to hold a sentence together from fragments read one at a time, which is far harder to follow than the same sentence read whole.
A scan of the page sees a named role="log" holding a greeting, a labelled field and a button, and finds nothing wrong: it is the pattern W3C describes for a chat history. The failure only exists while a reply streams, which means asking the assistant a question, and an audit doesn't do that on a live site. EaseWeb's audit raises a review lead when a live conversation sits beside a text field in something named as an AI assistant, so that a person asks it a question; it can't tell from the page whether replies stream. AI coding assistants asked to make a chat accessible can add role="log" or aria-live="polite" to the conversation, which is how this pattern is made. Component libraries for AI chat ship it too: the conversation component of Vercel's AI Elements, for one, is a role="log" that each reply streams into, as of October 2026. Only a screen reader, used while a reply streams, shows what is said.
Try it
Send the question, and listen to how the answer is read as it streams in.
The panel under the page lists each message a screen reader announces, in order. Count them: is the answer ever heard as one piece? Then apply each fix and send again.
Broken Can you fix this?
Fernhill help
Ask our assistant
Assistant: Hi! Ask me about orders, sizes or returns.
Screen reader says:
(Send the question)
The fix
Read the reply once, when it is complete.
<section class="assistant" aria-labelledby="assistant-title"> <h2 id="assistant-title">Ask our assistant</h2> <div class="messages" role="log" aria-label="Conversation"> <p class="message from-assistant"><span class="visually-hidden">Assistant: </span>Hi! Ask me about orders, sizes or returns.</p> </div> <form class="ask-form"> <label for="ask-input">Your question</label> <input id="ask-input" autocomplete="off"> <button>Send</button> </form></section>What is wrong2 points
The conversation is a log with a name, every message says who wrote it, and focus stays in the field. The stream is where it fails.
- Each chunk is appended to the reply inside the live log, so each one is announced as a message of its own: "Yes, within", then "30 days", then "even if you".
- Nothing is announced when the reply is complete, so people can't tell from what they heard whether the answer has ended or is still coming.
The finished reply, read once
<section class="assistant" aria-labelledby="assistant-title"> <h2 id="assistant-title">Ask our assistant</h2> <div class="messages" role="log" aria-label="Conversation" aria-live="off"> <p class="message from-assistant"><span class="visually-hidden">Assistant: </span>Hi! Ask me about orders, sizes or returns.</p> </div> <p id="reply-status" class="visually-hidden" role="status"></p> <form class="ask-form"> <label for="ask-input">Your question</label> <input id="ask-input" autocomplete="off"> <button>Send</button> </form></section>How it is fixed4 steps
- Add
aria-live="off"to the conversation. It stays a named log that people can find and read back through, but what is added to it is not announced while focus is elsewhere, so the stream is silent. - Add an empty, visually hidden
pwithrole="status"beside the conversation, outside it. It is in the page before the first question, so writing into it is a change that is announced, politely. - When the last chunk has arrived, write the whole reply into it. The reply's own text starts with the hidden "Assistant: ", so the screen reader reads "Assistant: Yes, within 30 days, even if you have used it…" once, in one piece, when it finishes what it was saying.
- Clear the region when a question is sent, so a reply that happens to match the last one word for word is still a change, and is still read.
This fixes the Before and passes 4.1.3: the reply is presented once, as a whole message, without moving focus. The longer the reply, the longer the wait before anything is heard, and the more there is to listen to in one go.
Why this fix: Say it once, when the change has settled
Both keep the stream silent and say one thing when it is complete; they differ in what that one thing is. Reading the finished reply suits an assistant whose answers are a sentence or two, like a shop's help assistant: people hear the answer without lifting a finger, as they would hear a message in any chat. It reads the reply as one flat run of words, though, so a list, a table or a block of code loses its shape, and a reply of several paragraphs is a long thing to sit through with no way back except reading it again. The short message suits an assistant whose answers are long or structured: people hear that the answer is ready and read it the way they read everything else, with headings, lists and code kept, at their own pace. Choose by the length and shape of your typical answer.
Not a fix: aria-busy. ARIA describes aria-busy="true" for this case: set it on the log while the reply streams, set it back to false when it is complete, and assistive technology may hold the changes and read them as one. The specification says "may", so a screen reader is free to read the changes as they come, and a log marked busy can still be read in pieces. It does no harm alongside either fix, but it is not a fix on its own.
Not a fix: making the reply bubble atomic. aria-atomic="true" on the bubble, or rewriting the whole reply into it on each chunk, makes each change read the reply again from its first word. People hear the opening phrase over and over as the reply grows.
The wait before the first word. Many assistants show a "Thinking…" indicator before the reply starts. Write it into the status region once, if the wait is long enough to need saying, and never put a repeating indicator or one that changes its text every second inside a live region. The wait itself is covered in loading spinners that screen readers never announce.
Stopped and failed replies. A Stop button ends the stream early; write "Stopped." or the partial reply into the status region, so people know the answer is not complete. When the stream fails, write what failed and what to do into the same region ("The assistant couldn't answer. Try again."). Put the write in a finally block, or after a try, so it runs however the stream ends.
Several replies at once. An assistant that answers in two or three messages, or that runs tools and reports each step, still says one thing per finished message. Don't announce each tool step, and don't stack messages in the status region: it holds the latest one.
Polite, not assertive. A reply is never urgent enough to cut into what people are reading. Don't use role="alert" or aria-live="assertive" for it.
Don't move focus to the reply. It does get the reply read, but it takes the caret out of the field, where people are already typing their next question.
Chats that never announce at all. A chat whose conversation is in no live region, and whose replies are never said, is a different failure: see chat widgets that never announce their messages. Its fixes use role="log" for replies that arrive whole; once replies stream, add the silencing and the status region from this guide.
Verify the fix
4 checks, no mouse.
Test with: Screen reader, Keyboard
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the chat component, the streaming code or the component library changes.
How screen readers handle role="status", role="log" and role="log" aria-live="off"
Limits
How a screen reader paces a run of live region changes, and whether it reads, queues or skips them, differs between screen readers, browsers and releases, so test the stream with the ones your users use. Auditors may record a reply read in pieces under 4.1.3, since what is presented is not the message, or as a usability finding, since the log does expose each change; either way, people can't follow the answer. This guide covers one assistant on one page; an assistant drawn in a vendor's frame can only be changed through the vendor's settings, and the questions to ask them are in chat widgets that never announce their messages. Scores, tickers and timers that change by themselves are a different case, covered in countdown timers that screen readers keep reading out.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.