Skip to content
  1. Keyline UI
  2. Bubble
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

Bubble

One message: a frame holding the text, then any files, then its reactions on the bottom keyline. Bubbles in a run share their edges, so the run is one stack rather than a pile of boxes. Tone comes from who sent it, never decoration, and a message on its way runs a pending segment along its foot.

Loading the live example…

"use client"

import { useState } from "react"
import { Avatar } from "@/components/keyline/ui/avatar"
import { Bubble, BubbleContent, BubbleReactions } from "@/components/keyline/ui/bubble"
import { Message, MessageAvatar, MessageContent, MessageHeader } from "@/components/keyline/ui/message"

export function PartyAnnouncement() {
  const [mine, setMine] = useState(false)
  const count = 4 + (mine ? 1 : 0)

  return (
    <div className="max-w-[520px]">
      <Message align="start">
        <MessageAvatar>
          <Avatar name="Angela Martin" decorative />
        </MessageAvatar>
        <MessageContent>
          <MessageHeader>
            <span>Angela Martin</span>
            <time dateTime="2026-11-10T09:05">9:05</time>
          </MessageHeader>
          <Bubble>
            <BubbleContent>The Party Planning Committee meets at 3 in the break room.</BubbleContent>
          </Bubble>
          <Bubble>
            <BubbleContent>Theme is decided. It is not up for discussion.</BubbleContent>
            <BubbleReactions>
              <button type="button" aria-pressed={mine} aria-label={`Thumbs up, ${count}${mine ? ", you reacted" : ""}`} onClick={() => setMine(!mine)}>
                👍 <span className="font-kl-mono">{count}</span>
              </button>
            </BubbleReactions>
          </Bubble>
        </MessageContent>
      </Message>
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/bubble

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { Bubble, BubbleContent, BubbleReactions } from "@/components/keyline/ui/bubble"
<Bubble tone="tint" pending={sending}>
  <BubbleContent>I'll put two cases on Thursday's truck.</BubbleContent>
  <BubbleReactions>…</BubbleReactions>
</Bubble>

Props

PropTypeDefaultDescription
tone"plain" | "tint" | "attention"Default "plain"plain for the other party, tint for the reader's own, attention for one that wasn't sent, with a ! cell before the text.
pendingbooleanDefault falseThe message is on its way. A static hatch under reduced motion.
renderReactElementDefault —On BubbleContent: a button or link, when the message itself acts, such as opening a thread.
BubbleReactionsdivDefault —A row of small cells on the bottom keyline. Use buttons with aria-pressed and a label that says the count.

Text takes its direction from its first strong letter (dir="auto"), so a message in Hebrew reads right to left in an English conversation. The Message page shows a failed bubble and its Retry.

Sheet 69 of 73PreviousMessageNextMessage scroller
On this page