Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Chat

Bubble

Displays conversational content in a message bubble. Supports variants, alignment, grouping, reactions, and collapsible content.

Playground

The install failure is coming from the workspace package โ€” its peers were never installed.
๐Ÿ‘๐Ÿ‘€
โ€”variant
โ€”align
โ€”BubbleReactions
โ€”side
โ€”align
โ€”dir
Usage
Bubble is the message surface and nothing else โ€” it frames chat text, short structured output, quoted replies, and suggestions. It sizes to its content up to 80% of the container width. Avatars, names, timestamps, and row-level actions belong to Message, which wraps a Bubble to build a full chat interface.
Did you remove the stale route?
Yes โ€” I checked the registry output and removed it.
import {
  Bubble,
  BubbleContent,
} from "@workspace/ui/components/bubble"

<Bubble variant="muted">
  <BubbleContent>Did you remove the stale route?</BubbleContent>
</Bubble>

<Bubble align="end">
  <BubbleContent>Yes โ€” I checked the registry output and removed it.</BubbleContent>
</Bubble>
Variants
Seven treatments, from a strong primary bubble down to unframed content. Every variant styles BubbleContent through the parent, so the variant lives on Bubble while the color lands on the inner surface. ghost drops the frame, padding, and max width so assistant text can span the full row.
This is the default primary bubble.
This is the secondary variant.
This one is muted. It uses a lower emphasis color for the bubble.
๐Ÿ‘
This one is tinted. The tint is a softer color derived from the primary color.
We can also use an outlined variant.
Or a destructive variant with a reaction.
๐Ÿ”ฅ

Ghost bubbles work for assistant text and other content that should not be framed.

They drop the max width, so a long reply can span the full row instead of wrapping inside a pill.

<Bubble>
  <BubbleContent>This is the default primary bubble.</BubbleContent>
</Bubble>

<Bubble variant="secondary" align="end">
  <BubbleContent>This is the secondary variant.</BubbleContent>
</Bubble>

<Bubble variant="muted">
  <BubbleContent>This one is muted.</BubbleContent>
  <BubbleReactions role="img" aria-label="Reaction: thumbs up">
    <span>๐Ÿ‘</span>
  </BubbleReactions>
</Bubble>

<Bubble variant="tinted" align="end">
  <BubbleContent>This one is tinted.</BubbleContent>
</Bubble>

<Bubble variant="outline">
  <BubbleContent>We can also use an outlined variant.</BubbleContent>
</Bubble>

<Bubble variant="destructive" align="end">
  <BubbleContent>Or a destructive variant with a reaction.</BubbleContent>
</Bubble>

<Bubble variant="ghost">
  <BubbleContent>Ghost bubbles work for unframed assistant text.</BubbleContent>
</Bubble>
When to use each variant
VariantDescription
defaultA strong primary bubble, usually for the current user.
secondaryThe standard neutral bubble for conversation content.
mutedA lower-emphasis bubble for quiet supporting content.
tintedA subtle primary-tinted bubble.
outlineA bordered bubble for secondary or rich content.
ghostUnframed content for assistant text or rich content. Removes the max width so it spans the full row.
destructiveA destructive bubble for an error or a failed action.
Alignment
align pushes the bubble to the start or the end of the conversation โ€” the incoming/outgoing split of a thread. It uses logical properties, so the sides mirror under dir="rtl". When you build a chat with Message, set alignment on the message row instead: the bubble reads the row's data-align and follows it.
This bubble is aligned to the start. This is the default alignment.
This bubble is aligned to the end. Use this for user messages.
<Bubble variant="muted">
  <BubbleContent>This bubble is aligned to the start. This is the default.</BubbleContent>
</Bubble>

<Bubble align="end">
  <BubbleContent>This bubble is aligned to the end. Use this for user messages.</BubbleContent>
</Bubble>
Bubble Group
BubbleGroup stacks consecutive bubbles from the same sender with a tighter gap. Alignment stays on each Bubble โ€” the group is only the column that holds them.
Can you tell me what's the issue?
You tell me!
It worked yesterday. You broke it!
Find the bug and fix it.
๐Ÿ‘€
Want me to diff yesterday's you against today's you?
<BubbleGroup>
  <Bubble align="end">
    <BubbleContent>You tell me!</BubbleContent>
  </Bubble>
  <Bubble align="end">
    <BubbleContent>It worked yesterday. You broke it!</BubbleContent>
  </Bubble>
  <Bubble align="end">
    <BubbleContent>Find the bug and fix it.</BubbleContent>
    <BubbleReactions align="start" role="img" aria-label="Reaction: eyes">
      <span>๐Ÿ‘€</span>
    </BubbleReactions>
  </Bubble>
</BubbleGroup>
Links and buttons
asChild on BubbleContent renders the surface as whatever you pass in. Use it for reply suggestions and link previews: the bubble picks up a hover tint and a focus ring for button and a children automatically.
How can I help you today?
<Bubble variant="tinted" align="end">
  <BubbleContent asChild>
    <button type="button" onClick={() => toast("You clicked forgot password")}>
      I forgot my password
    </button>
  </BubbleContent>
</Bubble>
Reactions
BubbleReactions is an absolutely positioned row anchored to a bubble edge: side picks the top or bottom edge and align the inline side. It holds emoji or quick-action buttons. Because the row overlaps the edge, leave extra vertical gap between bubbles โ€” these examples use gap-10.
I don't need tests, I know my code works.
๐Ÿ‘๐Ÿ˜ฎ
Bold. Fine, I'll add some tests and let you know when they're done.
๐Ÿ‘€๐Ÿš€+2
Tests passed on the first try. All 142 of them.
๐ŸŽ‰๐Ÿ‘
Are you sure I can run this command?
<Bubble variant="muted" align="end">
  <BubbleContent>I don't need tests, I know my code works.</BubbleContent>
  <BubbleReactions align="start" role="img" aria-label="Reactions: thumbs up, surprised">
    <span>๐Ÿ‘</span>
    <span>๐Ÿ˜ฎ</span>
  </BubbleReactions>
</Bubble>

<Bubble align="end">
  <BubbleContent>Tests passed on the first try. All 142 of them.</BubbleContent>
  <BubbleReactions side="top" align="start" role="img" aria-label="Reactions: party popper, clapping hands">
    <span>๐ŸŽ‰</span>
    <span>๐Ÿ‘</span>
  </BubbleReactions>
</Bubble>

<Bubble variant="destructive">
  <BubbleContent>Are you sure I can run this command?</BubbleContent>
  <BubbleReactions>
    <Button variant="ghost" size="xs" onClick={() => toast.success("Running commandโ€ฆ")}>
      Yes, run it
    </Button>
  </BubbleReactions>
</Bubble>
Show more
Long content composes with Collapsible for a show more/show less interaction. Keep the trigger inside BubbleContent so it inherits the bubble color, and render a truncated preview yourself rather than clipping with CSS โ€” the bubble grows to whatever it holds.
What did the review turn up?
The accessibility review found two focus states that were visually too subtle in dark mode. I checked the dialog, menu, and drawer paths because each one renders focusable control...
const [open, setOpen] = React.useState(false)

<Bubble variant="muted" align="end">
  <BubbleContent className="whitespace-pre-line">
    <Collapsible open={open} onOpenChange={setOpen}>
      <div>{open ? text : preview}</div>
      <CollapsibleTrigger asChild>
        <Button variant="link" className="gap-1 p-0 text-muted-foreground">
          {open ? "Show less" : "Show more"}
          <CaretDownIcon
            data-icon="inline-end"
            className="transition-transform group-data-[state=open]/button:rotate-180"
          />
        </Button>
      </CollapsibleTrigger>
    </Collapsible>
  </BubbleContent>
</Bubble>
Tooltip
A reaction row also carries metadata. Drop a Tooltip into BubbleReactions to reveal a read receipt or a timestamp on hover โ€” clear the row padding with className="p-0" when it holds a single icon button.
Did you remove the stale route?
Yes, removed it from the registry.
<Bubble align="end">
  <BubbleContent>Yes, removed it from the registry.</BubbleContent>
  <BubbleReactions className="p-0">
    <Tooltip>
      <TooltipTrigger asChild>
        <Button variant="ghost" size="icon-xs">
          <CheckIcon />
          <span className="sr-only">Read receipt</span>
        </Button>
      </TooltipTrigger>
      <TooltipContent>Read on Jan 5, 2026 at 4:32 PM</TooltipContent>
    </Tooltip>
  </BubbleReactions>
</Bubble>
Popover
Pair a bubble with a Popover to surface detail on demand โ€” the full stack trace behind a failed action, without spending the thread on it.
Run the build script.
Failed to run the command.
<Bubble variant="destructive">
  <BubbleContent>Failed to run the command.</BubbleContent>
  <BubbleReactions>
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="ghost"
          size="icon-xs"
          aria-label="Show error details"
          className="aria-expanded:text-destructive"
        >
          <InfoIcon />
        </Button>
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle className="text-sm">Command failed with exit code 1</PopoverTitle>
          <PopoverDescription className="text-sm">
            ENOENT: no such file or directory, open yarn.lock
          </PopoverDescription>
        </PopoverHeader>
      </PopoverContent>
    </Popover>
  </BubbleReactions>
</Bubble>
Accessibility
Bubble renders the presentational surface โ€” conversation semantics stay on the container around it. A reaction row reads as a string of unlabelled glyphs, and a counter like +8is announced as "plus eight", so group the row as a single image with role="img" and a descriptive aria-label; that also hides the individual emoji, so no aria-hidden is needed. When the reactions are interactive, render real buttons and label the icon-only ones. A clickable bubble must be a real button or a via asChild โ€” the bubble text becomes its accessible name, and the focus ring already ships. Finally, variants signal tone with color alone: keep the error context in the words of a destructive bubble.
<BubbleReactions role="img" aria-label="Reactions: thumbs up, fire, and 8 more">
  <span>๐Ÿ‘</span>
  <span>๐Ÿ”ฅ</span>
  <span>+8</span>
</BubbleReactions>

<BubbleReactions>
  <Button aria-label="Thumbs up" variant="secondary" size="icon-xs">
    <ThumbsUpIcon />
  </Button>
</BubbleReactions>

<Bubble variant="muted" align="end">
  <BubbleContent asChild>
    <button type="button" onClick={onReply}>
      I forgot my password
    </button>
  </BubbleContent>
</Bubble>
API Reference
Four div wrappers. Bubble mirrors its props to data-variant and data-align, which the content and reaction rows read to style and position themselves. All native div props pass through.
PartPropDefault
Bubblevariant?: "default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive""default"
Bubblealign?: "start" | "end""start"
BubbleclassName?: stringโ€”
BubbleContentasChild?: booleanfalse
BubbleContentclassName?: stringโ€”
BubbleReactionsside?: "top" | "bottom""bottom"
BubbleReactionsalign?: "start" | "end""end"
BubbleReactionsclassName?: stringโ€”
BubbleGroupclassName?: stringโ€”

Tokens used

15 design tokens consumed by the Bubble component.

  • --primary

    Primary

    bg-primarybg-primary/80
  • --primary-foreground

    Primary Foreground

    text-primary-foreground
  • --secondary

    Secondary

    bg-secondary
  • --secondary-foreground

    Secondary Foreground

    text-secondary-foreground
  • --muted

    Muted

    bg-muted
  • --foreground

    Foreground

    text-foreground
  • --background

    Background

    bg-background
  • --card

    Card

    ring-card
  • --border

    Border

    border-border
  • --input

    Input

    dark:border-inputdark:bg-input/30
  • --destructive

    Destructive

    bg-destructive/10text-destructive
  • --ring

    Ring

    focus-visible:border-ringfocus-visible:ring-ring/50
  • --radius-xl

    Radius Extra Large

    rounded-xl
  • --text-sm

    Text Small (14px)

    text-sm
  • --spacing

    Spacing unit (4px)

    gap-1px-3py-2px-1.5py-0.5start-3end-3