Siteplane UI

Bubble

Show the text of a chat message on a surface that fits its sender and purpose.

Markdown
Can you move the renewal review to Thursday?
Thursday works. I will update the invite.
"use client";

import { Bubble, BubbleContent } from "@/siteplane/components/bubble";

export default function Example() {
  return (
    <div className="flex w-full max-w-md flex-col gap-8">
      <Bubble>
        <BubbleContent>
          Can you move the renewal review to Thursday?
        </BubbleContent>
      </Bubble>
      <Bubble align="end" variant="default">
        <BubbleContent>Thursday works. I will update the invite.</BubbleContent>
      </Bubble>
    </div>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/bubble

Complete the one-time setup first. Registry JSON lists source files and dependencies.

Usage

import { Bubble, BubbleContent } from "@/siteplane/components/bubble";

<Bubble>
  <BubbleContent>Can you move the renewal review to Thursday?</BubbleContent>
</Bubble>;

Composition

BubbleGroup (optional, for consecutive bubbles)
└── Bubble
    └── BubbleContent

Bubble places the surface and BubbleContent draws it. A bubble is as wide as its text, up to 80 percent of the width it sits in, and a ghost bubble takes the full width. Long words and URLs wrap inside the bubble instead of widening it. In a conversation, put the bubble in a Message, which holds the avatar, the sender's name, the time and the actions of the turn.

Variants

Choose the variant by who wrote the message and what it holds.

  • muted, the default, is the quiet neutral fill, for the person's own turns in an AI chat and for the messages of the others in a conversation between people.
  • default is the accent fill with the accent's text color, for the person's own messages in a conversation between people.
  • tinted is a tint of the accent with the primary text color, a quieter choice than default for the person's own messages.
  • outline is the bordered message surface of the Card's message variant, with a small corner on the sender's side, for messages that need an edge of their own, such as on a filled surface.
  • ghost has no surface and takes the full width, for the assistant's text and for rich content such as lists, tables and code.
  • destructive is the error tint with the error text color, for a message or a reply that failed. Say what failed in the text, since the tint alone does not.
Can you send me last month's invoice?
Sure. It is attached to this conversation.
Thanks. Please add the purchase order number.
Added. The number is in the footer of the PDF now.
The September invoice is $1,240 for 31 seats on the Team plan. It was paid on October 2 with the card on file.
The invoice could not be generated. Try again in a few minutes.
"use client";

import { Bubble, BubbleContent } from "@/siteplane/components/bubble";

export default function Example() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Bubble align="end" variant="default">
        <BubbleContent>Can you send me last month's invoice?</BubbleContent>
      </Bubble>
      <Bubble variant="muted">
        <BubbleContent>
          Sure. It is attached to this conversation.
        </BubbleContent>
      </Bubble>
      <Bubble align="end" variant="tinted">
        <BubbleContent>
          Thanks. Please add the purchase order number.
        </BubbleContent>
      </Bubble>
      <Bubble variant="outline">
        <BubbleContent>
          Added. The number is in the footer of the PDF now.
        </BubbleContent>
      </Bubble>
      <Bubble variant="ghost">
        <BubbleContent>
          The September invoice is $1,240 for 31 seats on the Team plan. It was
          paid on October 2 with the card on file.
        </BubbleContent>
      </Bubble>
      <Bubble variant="destructive">
        <BubbleContent>
          The invoice could not be generated. Try again in a few minutes.
        </BubbleContent>
      </Bubble>
    </div>
  );
}

Alignment

align="end" places a bubble at the end of its row, for the person who is writing, and start is the default. Inside a Message with align="end", give the bubble the same align. The small corner of an outline bubble moves to the sender's side with it.

Groups

BubbleGroup stacks consecutive bubbles from one sender 8 px apart, such as a message sent in parts. Give each bubble its own align; the group does not set it. For consecutive turns that each have their own time or actions, use a Message Group instead.

Quick update on onboarding.
Fourteen new accounts finished setup last week, and none needed a manual fix.
Nice.
Which ones are still open?
"use client";

import {
  Bubble,
  BubbleContent,
  BubbleGroup,
} from "@/siteplane/components/bubble";

export default function Example() {
  return (
    <div className="flex w-full max-w-md flex-col gap-8">
      <BubbleGroup>
        <Bubble>
          <BubbleContent>Quick update on onboarding.</BubbleContent>
        </Bubble>
        <Bubble>
          <BubbleContent>
            Fourteen new accounts finished setup last week, and none needed a
            manual fix.
          </BubbleContent>
        </Bubble>
      </BubbleGroup>
      <BubbleGroup>
        <Bubble align="end" variant="default">
          <BubbleContent>Nice.</BubbleContent>
        </Bubble>
        <Bubble align="end" variant="default">
          <BubbleContent>Which ones are still open?</BubbleContent>
        </Bubble>
      </BubbleGroup>
    </div>
  );
}

AI SDK

The text parts of a UIMessage go into bubbles. The examples on this page show fixed messages; the code below takes them from useChat instead.

Text parts

The person’s text goes into a muted bubble at the end, and the assistant’s into a ghost bubble. A text part has state: "streaming" while it grows, and the bubble grows with it, without animation.

<MessageContent>
  {message.parts.map((part, index) =>
    part.type === "text" ? (
      <Bubble
        align={message.role === "user" ? "end" : "start"}
        key={index}
        variant={message.role === "user" ? "muted" : "ghost"}
      >
        <BubbleContent>{part.text}</BubbleContent>
      </Bubble>
    ) : null,
  )}
</MessageContent>

An assistant’s text is Markdown. Render it inside the ghost bubble with your Markdown renderer and the element map of Typeset, which sets it in the kit’s type.

Failed requests

When a request fails, status is "error". Show a destructive bubble with a short message of your own and Try again, as in the error turn of the Message page.

API reference

Component properties
PropTypeDefaultDescription
Bubble.variantdefault | muted | tinted | outline | ghost | destructivemutedThe surface of the message. ghost has no surface and takes the full width; the others are as wide as their text, up to 80 percent.
Bubble.align"start" | "end""start"The side of the row the bubble sits on. Inside a Message, give it the Message's align.
BubbleContentdiv props—Draws the surface: 8 px above and below the text, 16 px beside it, a 16 px radius and 14 px text on a 21 px line. Long words and URLs wrap.
BubbleGroupdiv props—Consecutive bubbles from one sender, 8 px apart. Each bubble keeps its own align.
--sp-bubble-padding-block / --sp-bubble-padding-inlinelength8 px / 16 pxThe space above and below and beside the text. Message reads the inline padding, so its header and footer start where the text starts.
--sp-bubble-radiuslength16 pxThe radius of the surface, the radius of a dialog. It follows the kit's radius setting.

Every part renders a div and accepts its props, a ref and className. Set the custom properties on a bubble or on any element around it. A bubble is not interactive: it takes no render prop and has no hover or focus state, so a link or a Button goes inside it or into the footer of its Message. The types BubbleVariant and BubbleAlign are exported.

Read the installed source.

Usage notes

A bubble is a surface without conversation semantics; the thread around it, such as the log of a Message Scroller, carries them. A variant's color never carries the meaning alone. Pair it with words, as a destructive bubble says what failed.