I can group messages, switch sides, and keep the whole thread easy to scan.
👍
Sure. Hit me with your best demo.
Yes. You are reading a demo that is demoing itself. Very meta. Very on-brand.
👍🔥👀+2
import { Bubble, BubbleContent,
The Bubble component displays framed conversational content. Use it for chat text, short structured output, quoted replies, suggestions, and reactions.
For full-featured chat interfaces, use the Message component. Bubble is intentionally scoped to the bubble surface. Place avatars, names, timestamps, metadata, and message-level actions in Message.
import { Bubble, BubbleContent, BubbleReactions } from "@/components/ui/bubble"
<Bubble> <BubbleContent> I checked the registry output and removed the stale route. </BubbleContent> <BubbleReactions> <span>👍</span> </BubbleReactions></Bubble>
Use variant to change the visual treatment of the bubble.
This is the default primary bubble.
This is the secondary variant.
This one is muted. It uses a lower emphasis color for the chat bubble.
👍
This one is tinted. The tint is a softer color derived from the primary color.
We can also use an outlined variant.
Or a destructive variant with a reaction.
🔥
Ghost bubbles work for assistant text, markdown, and other content that should not be framed.
This is perfect for assistant messages that should not have a frame and can take the full width of the container. You can also render code in it.
Ghost bubbles are full width and can take the full width of the container.
import { Markdown } from "@/components/markdown"import { Bubble,
Variant
Description
default
A strong primary bubble, usually for the current user.
secondary
The standard neutral bubble for conversation content.
muted
A lower-emphasis bubble for quiet supporting content.
tinted
A subtle primary-tinted bubble.
outline
A bordered bubble for secondary or rich content.
ghost
Unframed content for assistant text or rich content.
destructive
A destructive bubble for error or failed actions.
A bubble sizes to its content, up to 80% of the container width. The ghost variant removes the max-width so assistant text and rich content can span the full row.
Use align on Bubble to align the bubble to the start or end of the conversation.
This bubble is aligned to the start. This is the default alignment.
This bubble is aligned to the end. Use this for user messages.
import { Bubble, BubbleContent } from "@/components/ui/bubble"export function BubbleAlignmentDemo() {
align
Description
start
Align the bubble to the start of the conversation.
end
Align the bubble to the end of the conversation.
Note: When building chat interfaces, you probably want to use alignment on the Message component itself, not the Bubble component. You can use the role prop on the Message component to automatically align the bubble to the start or end of the conversation.
Use BubbleGroup to group consecutive bubbles from the same sender. Note the align prop should be set on the Bubble component itself, not the BubbleGroup component.
Use BubbleReactions for bubble reactions. You can use it to display reactions or quick action buttons. Use side and align to position the row, side="top" anchors it to the upper edge. Reactions overlap the bubble edge, so leave vertical space between rows, the examples below use a larger gap for this reason.
I don't need tests, I know my code works.
👍😮
Bold. Fine I'll add some tests. I'll let you know when they're done.
👀🚀+2
Tests passed on the first try. All 142 of them. Looking good!
Long bubble content can be composed with Collapsible to allow for a show more or show less interaction. Use the CollapsibleTrigger component to trigger the collapsible content.
How can I help you today?
The accessibility review found two focus states that were visually too subtle in dark mode.
I checked the dialog, menu, and drawer paths because each one renders focusable control...
Reactions render as a row of emoji. A screen reader reads each glyph with no context, and counters like +8 are announced as "plus eight". Group the row as a single image with a descriptive aria-label so it announces once. role="img" also hides the individual emoji from assistive tech, so no aria-hidden is needed.
When a bubble is clickable, render it as a real <button> or <a> with the render prop so it is focusable and exposes the correct role. BubbleContent ships a visible focus ring for interactive elements, and the accessible name comes from the bubble text. No extra label is needed.
<Bubble variant="muted" align="end"> <BubbleContent render={(props) => <button {...props} type="button" onClick={onReply} />} > I forgot my password </BubbleContent></Bubble>
Bubble variants signal role and tone with color. Pair them with text, alignment, or icons so meaning is not conveyed by color alone. For a destructive bubble, keep the error context in the message text rather than relying on the color treatment.