Bubble

A chat message, from the user or an assistant, with room for an avatar, a header and a footer.

Accessibility

What Bubble does for you, and what a chat built from it still needs from the page around it.

What Bubble renders

A Bubble is a stack of plain <div> elements with no role and no live region. A screen reader reads it as text in document order, and nothing tells it who sent the message or that a new one arrived. Bubble.List is the same: a <div> around a scrolling <div>.
  • The messages stay in document order even though the list scrolls from the bottom, so reading top to bottom follows the conversation.
  • Bubble passes role, aria-* and data-* props to its root. Bubble.List passes aria-* props, but its role prop is the sender map, so an ARIA role has to go on an element around it.

Announcing new messages

A sighted reader sees a reply arrive. A screen reader user hears nothing unless you add a live region. Wrap the list in an element with role="log", which is polite and announces what is added to it.
1
<div role="log" aria-label="Conversation">
2
<Bubble.List items={items} role={roles} />
3
</div>
  • A streamed reply grows a few characters at a time, and a live region may announce every step. If that is too noisy, keep the list out of the live region and announce the finished reply in a separate visually hidden role="status" element from onTypingComplete.
  • Announce the start of a reply too. The three-dot loading indicator has no text, so a reader hears nothing while they wait. Pass a loadingRender that includes visually hidden text like “Assistant is typing”.

Saying who sent it

placement and variant tell sighted readers who spoke. A screen reader hears neither, so the sender has to be in the text.
  • Put the sender's name in header, or in visually hidden text at the start of the bubble, like “You said” and “Assistant said”.
  • An avatar image needs an alt, or an empty alt when the name is already in the header.
  • Do not tell senders apart by color alone. The default variants for two roles can look alike under a custom theme, and a reader with a color vision deficiency loses the cue first.

Keyboard and scrolling

A Bubble has nothing to focus unless you put a control in it. The buttons you add to footer or extra bring their own focus and keys.
  • Give icon-only buttons in the footer an aria-label: “Copy”, “Retry”, “Good answer”.
  • The scroll area of Bubble.List is not focusable. If the messages hold no links or buttons, some browsers give a keyboard user no way to scroll it. Test with the arrow keys, and add a focusable control or a “scroll to latest” button if it does not move.
  • autoScroll stops following new messages once the reader scrolls up, so a keyboard user reading history is not pulled down by each reply.

The edit field

editable turns the content into a contenteditable element and moves focus into it, with the caret at the end. The confirm and cancel controls are real buttons.
  • The field has no role, no label and no focus outline, and no prop reaches it. A screen reader may not say it is editable, so tell the reader in text, for example a visually hidden role="status" message that says the message is being edited.
  • Give the field a visible focus style with a focus-within: outline on the content key of classNames, so a keyboard user can see where they are typing.
  • When editing ends, move focus back to the control that opened the editor. The bubble does not do this for you.

Motion

typing, the loading dots and the blinking cursor all move. A reader who has asked for reduced motion should not have to watch text crawl in.
  • Read prefers-reduced-motion and pass typing={false} when it is set. The text then appears in one step.
  • motion: false in your token file's seed turns off the theme's transitions, but not typing, which reveals text on a timer. Turn that off with the prop.