Skip to content

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 ​

WhereKeys
ComposerEnter sends, Shift+Enter adds a line. The paperclip, send and stop buttons are real buttons with names
Message listTab reaches links, file chips, the "Show earlier messages" button and every control of a card
Approval cardTab 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 cardTab and arrow keys through the options and the free-text field; Enter in the field sends a lone single-choice question
History rowsTab, 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 fieldEnter or leaving the field saves; Escape keeps the old title
Message toolbarThe row shows while the message holds keyboard focus (always under the newest message and on touch screens)
Feedback formTakes focus when it opens; Escape closes it and focus returns to the thumbs-down
Message editorCtrl or Cmd + Enter saves, Escape cancels
Export menuOpens with focus on the first item; Arrow Up / Down, Home, End move, Escape closes and returns to the button
Notifications menuA dialog opened by the bell button; Escape closes and returns focus
Terms gateA modal dialog: it takes focus, and Tab stays inside it
Agent pickerA button with aria-haspopup and aria-expanded; each agent is a button, the current one has aria-current

Screen readers ​

ElementSemantics
Message listrole="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 cardA hidden assertive live region says "The assistant needs your approval: send_email" or "The assistant has a question"
CardsA 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 bannerrole="alert" with a Dismiss button
Stop and interrupted noticesrole="status"
Notes about an answer (warnings)A list labelled "Notes about this answer"
Usage meterrole="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 chipsA 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 progressrole="progressbar" with a name and a value, in the composer and in the message bubble
Composer notices (waiting, answering, read-only)role="status"
HistoryA 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 toolbarA group labelled "Message actions"; thumbs are aria-pressed; copy and rating results are announced
Tool step waiting for youA clock icon labelled "Waiting for your answer"
Announcementscritical is role="alert", the others role="status"; the close button is labelled
Terms gaterole="dialog" with aria-modal, labelled by its title and described by its text
Legal linksA nav labelled "Legal"
ImagesA 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.
  • autoFocusInput focuses 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 with themeColors, keep a contrast ratio of at least 4.5:1 for text against the matching ...Bg tint and the page background.
  • Focus rings use the accent colour; choose an accentPrimary that is visible against both bgPrimary and bgSecondary.

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, renderImage and renderEmptyState replace 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. renderToolApproval and renderQuestion receive readOnly and resolve; honour both.
  • The iframe. An embedded chat needs a title on the <iframe>, and allow="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 md breakpoint).

Testing checklist ​

  1. Tab through a conversation with a pending approval: focus should land on the card, and Deny / Approve should be reachable and named.
  2. Turn on a screen reader and send a message: only the finished answer is read, not every chunk.
  3. Trigger an error (stop the server): the banner is announced.
  4. Open the history with the keyboard only: arrows, Home / End, Enter, F2 (if rename is on), Escape on mobile.
  5. Turn on "Reduce motion" in the OS and check that spinners stop.
  6. Switch to the light theme and check the status colours against your page.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub