A chat bubble for plain, short messages. Bubble is the chrome around one message: alignment and colour for who sent it, an avatar, a name and a time, a delivery status with Retry, grouped bubbles that merge their corners, and a footer for actions. It does not render structured replies. Message renders an AI turn from typed parts (markdown, tools, charts, sources) and Chat holds the whole conversation; use Bubble for person-to-person or simple text chats, or put a Response inside a Bubble when you want the bubble look around formatted text.
Bring your own model.
These components only show what you give them, in one message shape, and report what the person does. They never call a model or a server, so they work with any backend. They are built for how assistive technology handles streaming text, tool steps and approvals.
import { Avatar, Bubble, BubbleList } from "@rdloom/react";
export default function BubbleConversationExample() {
return (
<div className="flex w-full justify-center">
<BubbleList className="w-[28rem] max-w-full">
<Bubble from="assistant" name="Support" avatar={<Avatar name="Support" size="sm" decorative />} timestamp="2026-10-08T10:41:00" timestampText="10:41">
Hi, how can I help today?
</Bubble>
<Bubble from="user" name="You" timestamp="2026-10-08T10:42:00" timestampText="10:42">
My invoice shows the wrong address.
</Bubble>
<Bubble from="assistant" name="Support" avatar={<Avatar name="Support" size="sm" decorative />} timestamp="2026-10-08T10:43:00" timestampText="10:43">
I can fix that. Which address should it show?
</Bubble>
</BubbleList>
</div>
);
}Installation
npx rdloom add bubbleCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/bubble.jsonWorks, but without upgrade tracking.
Usage
import { Avatar, Bubble, BubbleList } from "@rdloom/react";
<div className="flex w-full justify-center">
<BubbleList className="w-[28rem] max-w-full">
<Bubble from="assistant" name="Support" avatar={<Avatar name="Support" size="sm" decorative />} timestamp="2026-10-08T10:41:00" timestampText="10:41">
Hi, how can I help today?
</Bubble>
<Bubble from="user" name="You" timestamp="2026-10-08T10:42:00" timestampText="10:42">
My invoice shows the wrong address.
</Bubble>
<Bubble from="assistant" name="Support" avatar={<Avatar name="Support" size="sm" decorative />} timestamp="2026-10-08T10:43:00" timestampText="10:43">
I can fix that. Which address should it show?
</Bubble>
</BubbleList>
</div>Grouped
import { Avatar, Bubble, BubbleGroup, BubbleList, Button } from "@rdloom/react";
export default function BubbleGroupedExample() {
return (
<div className="flex w-full justify-center">
<BubbleList className="w-[28rem] max-w-full">
<BubbleGroup aria-label="Messages from Alex">
<Bubble name="Alex" avatar={<Avatar name="Alex Rivera" size="sm" decorative />}>
Did you see the new design?
</Bubble>
<Bubble name="Alex" avatar={<Avatar name="Alex Rivera" size="sm" decorative />}>
I moved the filters to the top.
</Bubble>
<Bubble
name="Alex"
avatar={<Avatar name="Alex Rivera" size="sm" decorative />}
timestamp="2026-10-08T09:15:00"
timestampText="09:15"
actions={
<Button variant="ghost" size="sm">
Copy
</Button>
}
>
Tell me what you think.
</Bubble>
</BubbleGroup>
<BubbleGroup aria-label="Your messages">
<Bubble from="user">Looks good.</Bubble>
<Bubble from="user" timestamp="2026-10-08T09:16:00" timestampText="09:16">
Can we ship it today?
</Bubble>
</BubbleGroup>
</BubbleList>
</div>
);
}Failed retry
import { useState } from "react";
import { Bubble, BubbleList } from "@rdloom/react";
export default function BubbleFailedRetryExample() {
const [status, setStatus] = useState<"failed" | "sending" | "sent">("failed");
const retry = () => {
setStatus("sending");
setTimeout(() => setStatus("sent"), 1200);
};
return (
<div className="flex w-full justify-center">
<BubbleList className="w-[28rem] max-w-full">
<Bubble from="user" status={status} onRetry={retry} timestamp="2026-10-08T14:02:00" timestampText="14:02">
Please move the call to Friday.
</Bubble>
</BubbleList>
</div>
);
}With attachments
import { Attachment, AttachmentList, Bubble, BubbleList } from "@rdloom/react";
export default function BubbleWithAttachmentsExample() {
return (
<div className="flex w-full justify-center">
<BubbleList className="w-[28rem] max-w-full">
<Bubble
from="user"
name="You"
timestamp="2026-10-08T11:20:00"
timestampText="11:20"
attachments={
<AttachmentList aria-label="Sent files">
<Attachment name="brief.pdf" sizeText="640 KB" mediaType="application/pdf" href="#brief" />
<Attachment name="budget.xlsx" sizeText="88 KB" />
</AttachmentList>
}
>
Here are the two files.
</Bubble>
</BubbleList>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
fromuser sits at the end edge in the action colour; assistant sits at the start edge on a subtle surface. | "user" | "assistant" | "assistant" |
positionPlace in a run of consecutive bubbles from the same sender. Bubbles in a run merge the corners that touch. BubbleGroup sets it for you. | "single" | "first" | "middle" | "last" | "single" |
nameWho sent it. Shown above single and first bubbles, and used in the accessible name. | string | none |
avatarAn Avatar for the sender. Shown beside single and last bubbles; other bubbles in a run keep the space so text lines up. | node | none |
timestampWhen it was sent. Renders a time element with this value as dateTime. | Date | string | none |
timestampTextThe visible text for the time, e.g. "10:42". Without it the time is formatted in the reader's locale and time zone after the page loads (the server HTML leaves it empty, so the server and the browser never disagree). | string | none |
statussending shows "Sending". failed shows "Not sent" and a Retry button when onRetry is given. sent shows nothing. | "sending" | "sent" | "failed" | "sent" |
onRetryCalled when Retry is pressed on a failed bubble. | () => void | none |
actionsThe footer: buttons such as copy or regenerate. | node | none |
attachmentsFiles attached to the message, usually an AttachmentList. Shown inside the bubble below the text. | node | none |
childrenThe message content. | node | none |
Accessibility
Role article, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: moves to Retry and to the buttons in the footer
Screen readers announce
- "Alex message, article" then the text and the time
- "Not sent" then "Retry, button"
- "Sending"
What your code must do
- Each bubble is an article named by its sender, e.g. "Alex message" or "You message"
- The time is a real time element with a machine readable dateTime
- A failed message says "Not sent" in text, with an icon, and offers Retry as a named button; it is announced as an alert
- Sending is a polite status, not only a dimmed bubble
- Alignment uses start and end, so the bubbles mirror in right to left languages
- A run of bubbles in BubbleGroup is a labelled group and each bubble keeps its own name
- Text meets 4.5:1 on both bubble colours
Guidelines
Use it when
- A simple chat between people, or with a bot that only sends short text
- Showing delivery state and retry for each message
- Grouping several messages from the same sender
Avoid it when
- An AI reply with tools, tables, charts or sources: use Message
- A whole conversation with an input and scrolling: use Chat
Don't
- Putting a Bubble inside a Message, which already has its own bubble
- Showing the name and avatar on every message in a run
- Reporting a failure only with a red bubble
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-action-primary--rd-color-action-on-primary--rd-color-surface-subtle--rd-color-text-default--rd-color-text-muted--rd-color-border-default--rd-color-feedback-danger--rd-color-focus-ring--rd-radius-overlay