Bubble

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

Usage

When to reach for Bubble, how to build a conversation with Bubble.List, and what to watch for while a reply streams in.

When to use

A Bubble is one message in a chat: something the reader sent, or something an assistant or another person sent back. Use Bubble.List for the whole conversation, and a single Bubble only where one message stands alone.
  • A chat with an AI assistant, where replies stream in and may hold Markdown, code or charts.
  • A support or team thread, where who said what matters and each message carries a name, a time or an avatar.
  • A message the reader can act on: copy, retry, rate or edit it from the footer.
Use something else when
  • The text is a status for the page, not a turn in a conversation. That is an Alert.
  • The text is a hint attached to a control. That is a Tooltip or a Popover.
  • The list is a feed of comments or events with no back and forth. A plain list or a Timeline reads better than a column of bubbles.

Who said it

placement is the main signal for who sent a message, so keep it consistent: the reader's own messages at end, everyone else at start.
  • In Bubble.List, set it once per role in the role map rather than on every item.
  • In a thread with more than two people, side alone does not say who spoke. Add an avatar or a header with the name.
  • The avatar, the speech corner of shape="corner" and the default footer all follow placement, so you do not have to mirror them yourself.

Variant and shape

Pick one look per role and keep it. Mixing variants inside one sender's messages reads as two different senders.
  • filled suits short messages from the reader. outlined or borderless suits long AI answers, where a heavy fill behind several paragraphs gets tiring.
  • shadow lifts the bubble off a busy background, and it is the default for Bubble.System.
  • round suits one-line messages. On a message that wraps to several lines, the pill shape turns into a rounded box anyway.

Loading, typing and streaming

A reply goes through three states: waiting for the first token, receiving tokens, and done. Each one has its own prop.
  • loading covers the wait before the first token. Turn it off as soon as text arrives, or the reader stares at dots while the answer is already there.
  • typing animates text in. It only works on a string, so a contentRender that returns Markdown as a node shows the text at once.
  • Keep streaming on until the stream ends. Without it, onTypingComplete fires every time the animation catches up with the text received so far, which can be many times for one reply. With it, the callback fires once.
  • keepPrefix (on by default) keeps the part of the text that did not change on screen when content is replaced, so a growing string animates only its new end.

Building a conversation with Bubble.List

items holds the messages and role holds the look of each sender. The final props of a bubble are its role's, then the item's own on top.
  • Give every item a stable key. Use your message id, not the array index, so each bubble keeps its own state when messages are added or removed.
  • Do not name your own roles system or divider. Those two render Bubble.System and Bubble.Divider, whatever props you give them. Pick another name for a custom bubble.
  • A role can be a function of the item, so one role renders a different footer for a message that failed.
  • With useXChat, pass each message's status on the item. It reaches every slot function and contentRender through info.

Height and scrolling

Give the list a height through its parent, not on the list. With a fixed height on Bubble.List, a short conversation sits at the bottom of the box with empty space above it. Put the list in a flex column with a fixed height, or give it a max-height, and a short conversation starts at the top.
  • autoScroll (on by default) follows new messages while the reader is at the bottom, and leaves the view alone once they scroll up to read.
  • To jump somewhere yourself, take a ref and call scrollTo({ top: 'bottom' }) or scrollTo({ key }). A “scroll to latest” button is the usual place for it.

Editing a message

editable swaps the content for an editable field with confirm and cancel buttons. It is a switch you control, not a permission: true opens the editor right away.
  • Keep the editing state in your own state, set editable={editingKey === item.key}, and set it back in both onEditConfirm and onEditCancel. Otherwise the editor stays open after confirm.
  • Only a string content can be edited. A node or an object throws when the editor opens.
  • onEditConfirm hands you plain text with line breaks. Treat it like any user input before you send or render it.

Restyling it

Reach for the semantic keys before a descendant selector: root, avatar, body, header, content, footer, extra. A part with a key keeps its name across versions.
  • On Bubble.List, classNames styles every bubble at once, and a role or an item overrides it. Put per-sender styling in the role map.