Bubble
Displays conversational content in a message bubble — seven variants, start/end alignment, grouping, edge-anchored reactions, and collapsible content.
- Status
- Since
0.1.0- Accessibility pattern
- presentational surface
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Bubble before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify bubbleInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/bubbleRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
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>;Bubble is scoped to the bubble surface: framed chat text, short structured output, quoted replies,
suggestions and reactions. Avatars, names, timestamps and message-level actions belong to
Message, which owns the row around it.
Anatomy
Examples
Composition
Bubble is the container; BubbleContent is the surface that carries the text — the variant colours
the content element, so padding and the rounded corners stay put. BubbleReactions floats over the
bubble edge.
Bubble
├── BubbleContent
└── BubbleReactionsBubbleGroup stacks consecutive bubbles from the same sender. The align prop stays on each
Bubble, not on the group.
BubbleGroup
├── Bubble
│ └── BubbleContent
└── Bubble
└── BubbleContentFeatures
- Seven visual variants, from a strong primary bubble to unframed ghost content
- Start and end alignment for sender and receiver bubbles
- Reactions that anchor to the bubble edge with configurable side and alignment
- Bubbles size to their content, up to 80% of the container width
- Polymorphic content via
renderfor link and button bubbles - Customizable styling through the
classNameprop on every part
Variants
Use variant to change the visual treatment of the 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.
Alignment
Use align on Bubble to align the bubble to the start or end of the conversation.
| align | Description |
|---|---|
start | Align the bubble to the start of the conversation. |
end | Align the bubble to the end of the conversation. |
When building a chat interface, put the alignment on Message instead —
the row's data-align reaches the bubble through group-data-[align=end]/message:self-end.
Bubble Group
Use BubbleGroup to group consecutive bubbles from the same sender. The align prop belongs on each
Bubble, not on the group.
Links and Buttons
Turn a bubble into a link or a button with the render prop on BubbleContent. It renders a real
<button> or <a>, so it is focusable, exposes the correct role, and takes the global 2px
:focus-visible outline from base.css.
<Bubble variant="muted">
<BubbleContent render={<button type="button" />}>Click here</BubbleContent>
</Bubble>Reactions
BubbleReactions holds reactions or quick action buttons. side (top / bottom) and align
(start / end) position the row against the bubble edge. Reactions overlap that edge, so leave
vertical space between rows — the example below uses a larger gap for exactly that reason.
Show More / Collapsible
Long bubble content composes with Collapsible for a show
more / show less interaction. CollapsibleTrigger is the control, rendered as a link Button inside
the bubble surface.
Tooltip
Wrap a control in the reactions row in a Tooltip to reveal metadata on
hover, such as when a message was read.
Popover
Pair a bubble with a Popover to surface more information on demand,
such as the full error message behind a failed action.
API Reference
Bubble
The root bubble wrapper. A <div>; every other div prop passes through.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive" | "default" | The bubble visual treatment. |
align | "start" | "end" | "start" | The inline alignment of the bubble. |
className | string | — | Additional classes to apply to the root element. |
BubbleContent
The bubble content wrapper, built on Base UI's useRender.
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | function | — | Render the content as another element, such as a link or button. |
className | string | — | Additional classes to apply to the content element. |
BubbleReactions
Displays overlapped reactions for a bubble. A <div>; every other div prop passes through.
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "bottom" | "bottom" | The side of the bubble to anchor the reactions. |
align | "start" | "end" | "end" | The inline alignment of the reactions. |
className | string | — | Additional classes to apply to the reaction row. |
BubbleGroup
Groups consecutive bubbles from the same sender. It adds no props of its own — a <div> with
data-slot="bubble-group" and className.
Accessibility
Bubblerenders the presentational message surface. Conversation-level semantics — the list, the live region, the message count — belong to the container around it.- Reactions render as a row of emoji, and a screen reader reads each glyph with no context (
+8announces as "plus eight"). Group the row as a single image with a descriptivearia-label:role="img"also hides the individual glyphs, so noaria-hiddenis needed. - When reactions are interactive, render real buttons instead, and give an icon-only button an
aria-label. - An interactive bubble is a real
<button>or<a>throughrender, named by its own text. It takes the global 2px:focus-visibleoutline frombase.css— there is no ring glow, and nothing suppresses the outline (FOC-1 / FOC-6). - Variants signal role and tone with colour. Pair them with text, alignment or icons so meaning is
never conveyed by colour alone — a
destructivebubble keeps the error in the message text.
| Contract | States tested |
|---|---|
| Behaviour | default, error |
| Accessibility | focus-visible |
| Visual | default, focus, error, dark |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/bubble.patch. Every hunk:
- FOC-1, FOC-6 —
BubbleContentdrops[button,a]:outline-none,[button,a]:focus-visible:border-ring,[button,a]:focus-visible:ring-3and[button,a]:focus-visible:ring-ring/50. An interactive bubble takesbase.css's one 2px:focus-visibleoutline like every other control; upstream'soutline-nonewould have suppressed it outright, which is why the glow had to exist. - FOC-6 —
bubbleReactionsVariantsswaps upstream's RESTINGring-3 ring-cardforoutline-3 outline-card. FOC-6 is "never a box-shadow ring or glow anywhere" and it names Bubble by name; a TailwindringIS a box-shadow. The outline paints the identical 3px band in the identical token, outside the border box and following the radius, and the chip is a non-focusable<div>, so it can never collide with the focus outline. - A11Y-2 — audited, NO HUNK. Nothing in this file is a pointer target of its own:
Bubble,BubbleGroupandBubbleReactionsare<div>s, andBubbleContentbecomes a control only when the call site renders one into it, at which point the control's own size is the call site's.bubble.test.tsxmeasures the interactive-bubble fixture against the floor. - A11Y-13 — the
destructivebubble's ink becomes the family's-textrole. Upstream writestext-destructive, which on its own/10tint measures 3.987:1 in light — under the AA floor A11Y-1 enforces — wheretext-destructive-textreads 6.966:1 on the identical composite. The fill and every other part of the variant are untouched. - API-16 —
"use client"is ADDED. Upstream ships none, and this file callsuseRenderfrom@base-ui/react/use-render, which callsReact.useRefinternally — under thereact-servercondition that hook isundefined, so a server component rendering it throws on import.tooling/verify-rsc-safety.mjsis what found it. API-16 puts the boundary at the lowest interactive leaf, and this file is it. - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header the three-copy model needs; no decision of its own.