Skip to content
  1. Keyline UI
  2. Message
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

Message

One sender's run: their consecutive messages, close in time. The name, the time, every bubble's text and the receipt start on one vertical edge, so a run reads as one unit on either side. The other party sits at the start with an avatar; the reader's own run sits at the end. A footer says how the bubble above it stands.

Loading the live example…

"use client"

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

export function ThreadWithJim() {
  const [state, setState] = useState<"failed" | "sending" | "sent">("failed")

  const retry = () => {
    setState("sending")
    setTimeout(() => setState("sent"), 1200)
  }

  return (
    <div className="grid max-w-[520px] gap-5">
      <Message align="start">
        <MessageAvatar>
          <Avatar name="Jim Halpert" decorative />
        </MessageAvatar>
        <MessageContent>
          <MessageHeader>
            <span>Jim Halpert</span>
            <time dateTime="2026-11-10T10:42">10:42</time>
          </MessageHeader>
          <Bubble>
            <BubbleContent>Blue Cross wants the recycled stock samples by Friday.</BubbleContent>
          </Bubble>
          <Bubble>
            <BubbleContent>Can you check we have 20 lb in the warehouse?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message align="end">
        <MessageContent>
          <MessageHeader>
            <span>You</span>
            <time dateTime="2026-11-10T10:45">10:45</time>
          </MessageHeader>
          <Bubble tone="tint">
            <BubbleContent>Darryl says 30 cases, enough for samples.</BubbleContent>
          </Bubble>
          <Bubble tone={state === "failed" ? "attention" : "tint"} pending={state === "sending"}>
            <BubbleContent>I&apos;ll put two cases on Thursday&apos;s truck.</BubbleContent>
          </Bubble>
          {state === "failed" ? (
            <MessageFooter tone="attention">
              <span>Not sent: you&apos;re offline.</span>
              <button type="button" onClick={retry}>
                Retry
              </button>
            </MessageFooter>
          ) : (
            <MessageFooter role="status">{state === "sending" ? "Sending…" : "Sent"}</MessageFooter>
          )}
        </MessageContent>
      </Message>
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/message

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

Usage

import { Message, MessageAvatar, MessageContent, MessageFooter, MessageHeader } from "@/components/keyline/ui/message"
<Message align="start">
  <MessageAvatar><Avatar name="Jim Halpert" decorative /></MessageAvatar>
  <MessageContent>
    <MessageHeader><span>Jim Halpert</span><time>10:42</time></MessageHeader>
    <Bubble><BubbleContent>…</BubbleContent></Bubble>
  </MessageContent>
</Message>

Props

PropTypeDefaultDescription
align"start" | "end"Default "start"On Message: start for the other party, end for the reader's own. It mirrors in right-to-left text.
MessageAvatardivDefault —An Avatar, level with the first bubble. Mark it decorative: the header names the sender.
MessageHeaderdivDefault —The sender and a <time>.
tone"attention"Default —On MessageFooter: the bubble above wasn't sent. Give the reason, then a Retry button.

Put every consecutive message of a run in one Message: the header, then the bubbles, then at most one footer. Files go inside a bubble as Attachment cells.

Sheet 68 of 73PreviousDirectionNextBubble
On this page