bx

Bubble

The chat bubble: 7 variants (default, muted, tinted, outline, ghost, destructive, secondary) with optional reactions.

Hey there! What's up?
Hey! Want to see chat bubbles?
I can group messages, switch sides, and keep the whole thread easy to scan.
👍
tsx
<div className="flex w-full max-w-sm flex-col gap-8 py-6">
  <Bubble align="end">
    <BubbleContent>Hey there! What's up?</BubbleContent>
  </Bubble>
  <BubbleGroup>
    <Bubble variant="muted">
      <BubbleContent>Hey! Want to see chat bubbles?</BubbleContent>
    </Bubble>
    <Bubble variant="muted">
      <BubbleContent>
        I can group messages, switch sides, and keep the whole thread easy to scan.
      </BubbleContent>
      <BubbleReactions>
        <span role="img" aria-label="Thumbs up">👍</span>
      </BubbleReactions>
    </Bubble>
  </BubbleGroup>
</div>

Bubble is the colored rectangle that holds a message in a chat. It is a positioning wrapper (relative + max-width) that also applies the background and padding according to a variant.

Two expected children:

  • BubbleContent: the actual text or rich content of the message. In practice a plain <span>, since the styling (padding, rounded, bg) lives on the parent Bubble. This sub-component exists for API parity with shadcn and so that BubbleReactions can target [data-slot=bubble-content] in custom CSS.
  • BubbleReactions: an absolutely positioned badge (bottom right by default) that holds emojis or reaction buttons. It overflows the bubble via translate-y-3/4 and is framed by a ring-white. Optional.

7 variants define the look:

  • default: bg-primary text-primary-foreground. For messages sent by the current user (strong accent).
  • secondary: bg-secondary text-secondary-foreground. A less saturated alternative.
  • muted: bg-muted text-foreground. For received messages (other participants). The most common in multi-party chat.
  • tinted: bg-primary/10. Accent-tinted, softer than default. Emphasizes without shouting.
  • outline: border bg-background. A bordered rectangle with no fill, an "email draft" look.
  • ghost: no padding or background. For long assistant notes that blend into the text flow without a frame.
  • destructive: bg-destructive/10 text-destructive. For inline errors / warnings.

To stack several bubbles from the same author (runs of short messages, sequential reactions), wrap them in BubbleGroup, a flex flex-col gap-2 that keeps the rendering compact.

Combined with Message for left/right alignment and the avatar, it forms the primitive for any conversation thread.

Installation

Terminal
$ bext ui add bubble

Usage

tsx
import { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "@bext-stack/ui/primitives"

// Simple bubble
<Bubble variant="muted">
  <BubbleContent>Hey there! What's up?</BubbleContent>
</Bubble>

// Bubble with a reaction
<Bubble variant="muted">
  <BubbleContent>Alright, let me take a look.</BubbleContent>
  <BubbleReactions>
    <span role="img" aria-label="Thumbs up">👍</span>
  </BubbleReactions>
</Bubble>

// Group of bubbles from the same author
<BubbleGroup>
  <Bubble variant="muted"><BubbleContent>Hi there.</BubbleContent></Bubble>
  <Bubble variant="muted"><BubbleContent>How are you?</BubbleContent></Bubble>
</BubbleGroup>

Anatomy

<Bubble variant="default|secondary|muted|tinted|outline|ghost|destructive" align="start|end">
  ├── <BubbleContent>text or rich content</BubbleContent>
  └── <BubbleReactions>       // optional, absolute badge
      └── <span role="img" aria-label="Thumbs up">👍</span>   // emoji or <button>

<BubbleGroup>                 // stacks 2+ Bubbles from the same author
  ├── <Bubble>...</Bubble>
  └── <Bubble>...</Bubble>

API Reference

Bubble

Prop Type Default Description
variant "default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive" "default" Sets the background, the text color, and whether there is a border and padding.
align "start" | "end" "start" Written to data-align. When passed explicitly it also adds self-start or self-end, which positions the bubble in a flex column. Inside a Message with align="end" the bubble is pushed to the right by the Message, with or without this prop.
className string — Additional CSS classes on the root <div data-slot="bubble">.

BubbleContent

Prop Type Default Description
className string — Semantic wrapper for the content (span). No styling of its own: padding and bg live on Bubble.

BubbleGroup

Prop Type Default Description
className string — flex-col gap-2 container for stacking several Bubbles from the same author.

BubbleReactions

Prop Type Default Description
side "top" | "bottom" "bottom" Vertical position relative to the bubble. "bottom" → below, "top" → above.
align "start" | "end" "end" Horizontal alignment. "start" → left-3, "end" → right-3.
ariaLabel string — Names the group of reactions: when set, the container gets role="group" and this aria-label.
className string — Additional CSS classes. The badge is absolute z-10 with bg-muted + ring-white.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-slot every sub-component "bubble" | "bubble-content" | "bubble-group" | "bubble-reactions" Stable selector for custom styling.
data-variant Bubble "default" | "secondary" | ... Reflects the variant prop. Useful for custom overrides via [data-variant=X] { ... }.
data-align Bubble + BubbleReactions "start" | "end" Reflects the align prop. Lets custom CSS target each alignment.
data-side BubbleReactions "top" | "bottom" Vertical position.

Keyboard

Key Action
Tab Focuses the first button in BubbleReactions (if present).
Enter / Space Activates the focused button in a reaction.

Accessibility

A Bubble on its own is a visual container with no ARIA role. The text is directly readable by assistive technologies.

BubbleReactions accepts ariaLabel, which turns the container into a named role="group" (for example "Reactions"). Each reaction still needs its own name on the child you pass in. For a static emoji: <span role="img" aria-label="Thumbs up">👍</span>. For a reaction button: an aria-label on the <button>.

The destructive variant uses text-destructive, which has sufficient contrast in light and dark mode (validated WCAG AA on the shadcn tokens). Check it again if you override the palette.

For a complete accessible thread, wrap the Bubbles in a <Message> with an avatar (to identify the author) and set role="log" aria-live="polite" on the thread container.

Examples

Variants

Default (primary bg, sent messages)
Secondary (secondary background)
Muted (received messages, the most common)
Tinted (softened accent)
Outline (border, email look)
Ghost (no styling, assistant notes)
Destructive (errors / warnings)
tsx
<div className="flex flex-col gap-3 w-full max-w-md">
  <Bubble><BubbleContent>Default (primary bg, sent messages)</BubbleContent></Bubble>
  <Bubble variant="secondary"><BubbleContent>Secondary (secondary background)</BubbleContent></Bubble>
  <Bubble variant="muted"><BubbleContent>Muted (received messages, the most common)</BubbleContent></Bubble>
  <Bubble variant="tinted"><BubbleContent>Tinted (softened accent)</BubbleContent></Bubble>
  <Bubble variant="outline"><BubbleContent>Outline (border, email look)</BubbleContent></Bubble>
  <Bubble variant="ghost"><BubbleContent>Ghost (no styling, assistant notes)</BubbleContent></Bubble>
  <Bubble variant="destructive"><BubbleContent>Destructive (errors / warnings)</BubbleContent></Bubble>
</div>

Alignment (start / end)

Received: stays on the left
Sent: pushed to the right by align
tsx
// Outside a Message, an explicit align adds self-start / self-end in a flex column.
<div className="flex flex-col gap-3 w-full max-w-md">
  <Bubble align="start" variant="muted">
    <BubbleContent>Received: stays on the left</BubbleContent>
  </Bubble>
  <Bubble align="end">
    <BubbleContent>Sent: pushed to the right by align</BubbleContent>
  </Bubble>
</div>

BubbleGroup: stacking bubbles from the same author

Hi!
See my 2 bubbles stacked together?
Like a run of messages in iMessage.
tsx
<BubbleGroup>
  <Bubble variant="muted"><BubbleContent>Hi!</BubbleContent></Bubble>
  <Bubble variant="muted"><BubbleContent>See my 2 bubbles stacked together?</BubbleContent></Bubble>
  <Bubble variant="muted"><BubbleContent>Like a run of messages in iMessage.</BubbleContent></Bubble>
</BubbleGroup>

Reaction placement

Alright, let me take a look.
👍
You crushed it 🎉
❤️
tsx
// Default: side="bottom" align="end"
<Bubble variant="muted">
  <BubbleContent>Alright, let me take a look.</BubbleContent>
  <BubbleReactions>
    <span role="img" aria-label="Thumbs up">👍</span>
  </BubbleReactions>
</Bubble>

// Reaction at the top left:
<Bubble align="end">
  <BubbleContent>You crushed it 🎉</BubbleContent>
  <BubbleReactions side="top" align="start">
    <span role="img" aria-label="Heart">❤️</span>
  </BubbleReactions>
</Bubble>

Ghost (assistant long-form)

Ghost bubbles blend into the text flow with no frame. Useful for long assistant notes that should read as text, not as an individual message in a conversation.
tsx
<Bubble variant="ghost">
  <BubbleContent>
    Ghost bubbles blend into the text flow with no frame. Useful for long
    assistant notes that should read as text, not as an individual message
    in a conversation.
  </BubbleContent>
</Bubble>