Skip to content

Bubble

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.

AIv0.1.0experimentalWCAG 2.2 AAView spec

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.

Support
Hi, how can I help today?
You
My invoice shows the wrong address.
Support
I can fix that. Which address should it show?
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 bubble

Copies 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.json

Works, 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

Alex
Did you see the new design?
I moved the filters to the top.
Tell me what you think.
Looks good.
Can we ship it today?
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

Please move the call to Friday.
Not sent
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

You
Here are the two files.
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.

PropTypeDefault
from

user sits at the end edge in the action colour; assistant sits at the start edge on a subtle surface.

"user" | "assistant""assistant"
position

Place 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"
name

Who sent it. Shown above single and first bubbles, and used in the accessible name.

stringnone
avatar

An Avatar for the sender. Shown beside single and last bubbles; other bubbles in a run keep the space so text lines up.

nodenone
timestamp

When it was sent. Renders a time element with this value as dateTime.

Date | stringnone
timestampText

The 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).

stringnone
status

sending shows "Sending". failed shows "Not sent" and a Retry button when onRetry is given. sent shows nothing.

"sending" | "sent" | "failed""sent"
onRetry

Called when Retry is pressed on a failed bubble.

() => voidnone
actions

The footer: buttons such as copy or regenerate.

nodenone
attachments

Files attached to the message, usually an AttachmentList. Shown inside the bubble below the text.

nodenone
children

The message content.

nodenone

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