Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Bubble

A chat speech bubble — seven token-driven variants (incl. a brand-tinted surface), start/end alignment, interactive content, and a floating reactions chip.

Status
stable
Since
0.1.0
Accessibility pattern
presentational surface

Last updated

Default bubble
Secondary bubble
Muted bubble
Tinted bubble
Outline bubble
Ghost bubble
Destructive bubble

Install

Add Bubble from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/bubble

Usage

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

<Bubble variant="tinted" align="end">
  <BubbleContent>On my way.</BubbleContent>
</Bubble>;

Anatomy

A Bubble skins its BubbleContent child via the variant — the surface colour lives on the content element so padding and the rounded corners stay put.

Bubble — data-slot="bubble"
BubbleContent
BubbleGroup — data-slot="bubble-group"
BubbleReactions — data-slot="bubble-reactions"
  • Bubble — the container (data-slot="bubble", data-variant, data-align). Inside a Message the row's align is inherited.
  • BubbleContent — the rounded surface that carries the text. Built on Base UI useRender: pass render={<button />} (or an a) for an interactive bubble — it gets a hover surface, the global :focus-visible outline, and a focus border tint.
  • BubbleGroup — stacks consecutive bubbles from one sender.
  • BubbleReactions — a floating reactions chip pinned to a bubble corner (side, align).

Examples

Variants

default (the dark "sent" surface), secondary, muted, tinted (a brand surface), outline, ghost, and destructive.

Default bubble
Secondary bubble
Muted bubble
Tinted bubble
Outline bubble
Ghost bubble
Destructive bubble

Alignment

align="start" for received, align="end" for sent. Inside a Message the row's alignment is inherited automatically.

Received — hugs the start edge.
Sent — hugs the end edge.

Grouped

BubbleGroup stacks consecutive bubbles from the same sender.

Are we still on for 3pm?
I can move it earlier if that helps.
Let me know!
3pm works — see you then.

Conversation with reactions

Sent / received alignment, grouped, with a BubbleReactions chip.

Nice work on the launch! 🚀
Thanks — couldn't have done it alone.

Reaction placement

BubbleReactions pins to a corner via side (top/bottom) and align (start/end); it also holds multiple emoji or an overflow count.

Bottom-start reaction
Top-end, multiple reactions
Overflow count

Interactive (quick replies)

Render BubbleContent as a button (or an a) for tappable quick-reply or link bubbles — they get a hover surface and the global :focus-visible outline.

Want me to deploy to production?

Motion

Pass animateIn on a bubble you append after mount — a streamed reply, or one the user just sent — to fade + rise it in (motion-enter-up). It's off by default: an existing transcript must not animate every bubble on page load. Message takes the same prop, so set either or both.

Collapsible

Compose a Collapsible inside a bubble for "show more / less" on long messages.

Here's the gist: the migration moves all timestamps to UTC and backfills the new column.

With a popover

Hang a Popover off a reaction chip — e.g. error details on a failed message.

Couldn't send your message.

With a tooltip

A read-receipt check icon in a reaction chip with a Tooltip.

Heading out now — see you soon!

Playground

Try all seven surface variants, both alignments, and the opt-in entry animation — each change replays it — then copy the generated JSX.

On my way — be there in five.
<Bubble>
  <BubbleContent>On my way — be there in five.</BubbleContent>
</Bubble>

API Reference

Bubble

PropTypeDefaultDescription
align"end" | "start"'start'Which side the bubble hugs. - start: received (default). - end: sent — self-aligns to the end edge.
animateInbooleanfalseOpt-in entry animation (motion-enter-up, a fade + slight rise) for a bubble that is newly appended to a live thread. **Default off**: an existing transcript rendered on page load must not animate every bubble. Enable it only on bubble(s) you append after mount — e.g. a streamed reply or a message the user just sent (mirrors Message's animateIn; set either or both).
variantBubbleVariant'default'Surface skin. - default: dark neutral, for sent messages. - secondary / muted: neutral received surfaces. - tinted: brand-tinted received surface. - outline / ghost: quiet surfaces. - destructive: error / failed message.

Data attributes and CSS variables on Bubble

AttributeValues
data-alignmirrors a prop or state value
data-slot"bubble"
data-variantmirrors a prop or state value

BubbleContent

The render prop turns the surface into an interactive element — pass render={<button />} (or an a) for a tappable quick-reply or link bubble; it then gets a hover surface and the global :focus-visible outline.

PropTypeDefaultDescription
renderuseRender.RenderProp<Record<string, unknown>>Render the content surface as a different element (e.g. a button for an interactive bubble, or an a for a link bubble) via Base UI render.

BubbleReactions

PropTypeDefaultDescription
align"end" | "start"'end'Horizontal anchor relative to the bubble.
side"bottom" | "top"'bottom'Vertical anchor relative to the bubble.

Data attributes and CSS variables on BubbleReactions

AttributeValues
data-alignmirrors a prop or state value
data-sidemirrors a prop or state value
data-slot"bubble-reactions"

BubbleGroup

BubbleGroup is a layout wrapper with no own props — it accepts standard <div> props (className, children, …).

Accessibility

  • A non-interactive BubbleContent is a plain surface. Render it as a button/a only when it is genuinely actionable — the interactive surface never strips the outline: it keeps the global :focus-visible outline and additionally tints its border with the ring token ([button,a]:focus-visible:border-ring/(--alpha-tint-border)).
  • Variant colours are token-driven and contrast-checked in both themes; the tinted surface uses the brand family and stays readable light + dark.
  • BubbleReactions is presentational — give interactive reaction controls their own accessible labels.
ContractStates tested
Behaviourdefault, error
Accessibilityfocus-visible
Visualdefault, focus, error, dark

Do / Don't

Do
Use Bubble inside a Message, picking a variant for sent vs received and align for the side.
Don't
Use Bubble for non-conversational callouts — reach for Alert or Card instead.

On this page