Accessibility
What Chat UI does for keyboard and screen-reader users, where focus goes, how motion and colour are handled, and what stays your job when you replace parts of the UI.
New in 0.4.0
Labelled controls, a live message log, announced and focused approval cards, keyboard-operable history rows and menus, prefers-reduced-motion, and status colours that are readable on both themes.
This page describes behaviour the code implements; it is not a conformance statement. Test your own page with the keyboard and a screen reader, especially if you use render props.
Keyboard
| Where | Keys |
|---|---|
| Composer | Enter sends, Shift+Enter adds a line. The paperclip, send and stop buttons are real buttons with names |
| Message list | Tab reaches links, file chips, the "Show earlier messages" button and every control of a card |
| Approval card | Tab to Details, the remember checkbox, "Add a reason", Deny and Approve. Space or Enter on a button decides, so the card itself (not a button) takes focus when it appears |
| Question card | Tab and arrow keys through the options and the free-text field; Enter in the field sends a lone single-choice question |
| History rows | Tab, Enter, Space open a conversation; Arrow Up / Down, Home, End move between rows; F2 starts a rename; Escape closes the sidebar where it is an overlay |
| History rename field | Enter or leaving the field saves; Escape keeps the old title |
| Message toolbar | The row shows while the message holds keyboard focus (always under the newest message and on touch screens) |
| Feedback form | Takes focus when it opens; Escape closes it and focus returns to the thumbs-down |
| Message editor | Ctrl or Cmd + Enter saves, Escape cancels |
| Export menu | Opens with focus on the first item; Arrow Up / Down, Home, End move, Escape closes and returns to the button |
| Notifications menu | A dialog opened by the bell button; Escape closes and returns focus |
| Terms gate | A modal dialog: it takes focus, and Tab stays inside it |
| Agent picker | A button with aria-haspopup and aria-expanded; each agent is a button, the current one has aria-current |
Screen readers
| Element | Semantics |
|---|---|
| Message list | role="log", aria-live="polite", labelled "Conversation". While a reply streams it is aria-busy, so the finished answer is read once instead of every chunk |
| A new approval or question card | A hidden assertive live region says "The assistant needs your approval: send_email" or "The assistant has a question" |
| Cards | A labelled group; how a card ended (Approved, Denied, Expired, ...) is a role="status" line; problems in the argument editor are a role="alert" list |
| Error banner | role="alert" with a Dismiss button |
| Stop and interrupted notices | role="status" |
| Notes about an answer (warnings) | A list labelled "Notes about this answer" |
| Usage meter | role="progressbar" with a value and text ("$1.25 of $5.00 used"); the notice is role="status"; an invalid new limit is an alert |
| File chips | A list labelled "Attached files"; each chip has its state: uploading (status with the file name), ready, failed (with the reason and a retry button) |
| Upload progress | role="progressbar" with a name and a value, in the composer and in the message bubble |
| Composer notices (waiting, answering, read-only) | role="status" |
| History | A labelled list of buttons; badges are text ("Waiting for you"), not only colour; pin buttons are aria-pressed; select mode rows are labelled checkboxes and the count is announced |
| Message toolbar | A group labelled "Message actions"; thumbs are aria-pressed; copy and rating results are announced |
| Tool step waiting for you | A clock icon labelled "Waiting for your answer" |
| Announcements | critical is role="alert", the others role="status"; the close button is labelled |
| Terms gate | role="dialog" with aria-modal, labelled by its title and described by its text |
| Legal links | A nav labelled "Legal" |
| Images | A thumbnail link says "Open image in a new tab: name" |
Cards that arrive with a loaded conversation (after a reload or when opening one from the history) are not announced and do not take focus: only a card that appears while you are on screen is.
Focus
- A card that appears while the person is not typing takes focus: a question on its first control, an approval on the card itself. If they are typing in the composer or a field, focus is left alone.
- Once the last card is answered (or lapses) focus returns to the composer, unless it was moved somewhere else on purpose.
- Closing the feedback form, the export menu or the notifications dialog returns focus to the button that opened it.
autoFocusInputfocuses the composer on mount and after "New Chat" or selecting a conversation. It is off by default so a chat on a page does not steal focus.- A refused message or answer keeps the typed text in the composer, so nothing is lost for a keyboard user who has to try again.
Motion
Spinners, pulsing dots and the status badge pulse stop under prefers-reduced-motion: reduce (motion-reduce:animate-none), and the fade of the message-action row has no transition. The auto-scroll to new messages uses smooth scrolling.
Colour and contrast
- Outcomes never rely on colour alone: every state has an icon and text ("Approved", "Expired", "Limit reached").
- The light theme ships darker status colours (
success #047857,danger #b91c1c,warning #92400e,info #1d4ed8) so text stays readable on white. If you override them withthemeColors, keep a contrast ratio of at least 4.5:1 for text against the matching...Bgtint and the page background. - Focus rings use the accent colour; choose an
accentPrimarythat is visible against bothbgPrimaryandbgSecondary.
Language
Every label, notice and announcement is translated with the rest of the UI (English and Turkish are built in; others through customMessages), so a screen reader hears them in the person's language. The library does not set the page's lang attribute: set <html lang> yourself, and keep it in step with the chat's locale (onLocaleChange). See Internationalization.
What stays your job
- Render props.
renderToolApproval,renderQuestion,renderStopNotice,renderBudgetBanner,renderMessageActions,renderImageandrenderEmptyStatereplace the accessible built-ins. Give your replacements names, roles, keyboard access and (for anything that appears while the person is on screen) a live region.renderToolApprovalandrenderQuestionreceivereadOnlyandresolve; honour both. - The iframe. An embedded chat needs a
titleon the<iframe>, andallow="clipboard-write"if you want the copy button to use the clipboard API. - Colours you override, as above.
- Touch targets and zoom. The chat fills its container; give it a size that works at 200% zoom and on small screens (it uses an overlay sidebar below the
mdbreakpoint).
Testing checklist
- Tab through a conversation with a pending approval: focus should land on the card, and Deny / Approve should be reachable and named.
- Turn on a screen reader and send a message: only the finished answer is read, not every chunk.
- Trigger an error (stop the server): the banner is announced.
- Open the history with the keyboard only: arrows, Home / End, Enter, F2 (if rename is on), Escape on mobile.
- Turn on "Reduce motion" in the OS and check that spinners stop.
- Switch to the light theme and check the status colours against your page.

