fix(a11y): announce that a reply is generating, and that one failed - #6135
Merged
timothycarambat merged 4 commits intoAug 19, 2026
Merged
Conversation
The assistant's turn is currently invisible to a screen reader from start to finish. There is no live region anywhere in the frontend: no aria-live, no role="status", no role="log", no role="alert" and no aria-atomic in any .jsx or .js outside node_modules, across 589 files. The pending state is the sharpest case. PromptReply renders it as a bare dot-falling animation, which is purely visual, so a user who cannot see it is told neither that the assistant is working nor, later, that it stopped. The error state has the same problem for the opposite reason: the turn ends and nothing announces that it ended badly. This is a gap rather than a position. The same codebase already carries 56 aria-label attributes plus aria-hidden, aria-checked, aria-pressed and aria-current, so the intent is clearly there. Two changes, both in PromptReply: - The pending branch gains an sr-only role="status" carrying the text, with the animation marked aria-hidden. The text sits in the status region rather than wrapping the animation so that it announces once, on appearance, instead of on every repaint. - The error branch gains role="alert". Alert rather than status because a failed reply is the one case that warrants interrupting: nothing else on the page changes to signal the turn is over. Deliberately NOT included: announcing the streamed reply text itself. Piping a token stream into a live region makes a screen reader re-read the growing partial message on every chunk, which is worse than silence. Doing it properly means announcing the completed message once, and that belongs in ChatHistory where completion is observable, not here. Happy to follow up with it if wanted. The string is added to the en locale only; i18n.js sets fallbackLng "en", so the other 31 locales fall back rather than break.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
The assistant's turn is invisible to a screen reader from start to finish.
There is no live region anywhere in the frontend. No
aria-live, norole="status", norole="log", norole="alert", noaria-atomic, in any.jsxor.jsoutsidenode_modules, across 589 files.The pending state is the sharpest case.
PromptReplyrenders it as a baredot-fallinganimation:That is purely visual, so a user who cannot see it is told neither that the assistant is working nor, later, that it has stopped. The error branch has the same problem from the other end: the turn ends and nothing announces that it ended badly.
I want to be clear that this reads as a gap rather than a position. The same codebase already carries 56
aria-labelattributes plusaria-hidden,aria-checked,aria-pressedandaria-current, andsr-onlyis already used inPromptInputandToggle. The intent is plainly there. This is one thing that was never wired up.What changed
Two attributes and a string, both in
ChatHistory/PromptReply/index.jsx.Pending gains an
sr-onlyrole="status"carrying the text, and the animation is markedaria-hidden:The text sits inside the status region rather than the region wrapping the animation, so it announces once on appearance instead of on every repaint.
Error gains
role="alert". Alert rather than status, because a failed reply is the one case here that warrants interrupting the user: nothing else on the page changes to tell them the turn is over.What this deliberately does not do
It does not announce the streamed reply text. Piping a token stream into a live region makes a screen reader re-read the growing partial message on every chunk, which is worse than silence.
Doing that properly means announcing the completed message once, and the completion boundary is not observable from inside
PromptReply: this component re-renders on every chunk asreplygrows, and is swapped forHistoricalMessagewhen the turn ends. So it belongs inChatHistory, in a separate change. Happy to follow up with it if you want it, and happy to leave it if you would rather that stayed as it is.Verification
npx prettier --checkon both changed filesnpx eslinton both changed filesnpx vite buildWhat I have not done, stated plainly: I have not exercised this with a screen reader against a running instance. Producing a real stream needs an LLM provider credential I do not have. The change is two ARIA attributes and a translation string with no behavioural logic, and the reasoning above is about which region type fires when, so I would rather say that outright than imply a test I did not run. If a maintainer with a running instance wants to sanity-check it with VoiceOver or NVDA before merging, that seems entirely reasonable to me.
i18n
The string is added to
locales/en/common.jsonly.i18n.jssetsfallbackLng: "en", so the other 31 locales fall back rather than break. Say the word if you would prefer all 32 populated in this PR.