bx

Message

A single chat turn with a role avatar and a bubble styled per sender.

@meME
Deploying to prod real quick.
@rabbitR
It's 4:55 PM. On a Friday.
@meME
It's a one-line change.
Delivered
@rabbitR
It's always a one-line change 😭.
Alright, let me take a look.
👍
Oliver is typing…
tsx
<div className="flex w-full max-w-sm flex-col gap-6 py-6">
  <Message align="end">
    <MessageAvatar>
      <Avatar><AvatarImage src="/avatars/10.png" alt="@me" /><AvatarFallback>ME</AvatarFallback></Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble><BubbleContent>Deploying to prod real quick.</BubbleContent></Bubble>
    </MessageContent>
  </Message>
  <Message>
    <MessageAvatar>
      <Avatar><AvatarImage src="/avatars/02.png" alt="@rabbit" /><AvatarFallback>R</AvatarFallback></Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble variant="muted"><BubbleContent>It's 4:55 PM. On a Friday.</BubbleContent></Bubble>
    </MessageContent>
  </Message>
  <Message align="end">
    <MessageAvatar>…</MessageAvatar>
    <MessageContent>
      <Bubble><BubbleContent>It's a one-line change.</BubbleContent></Bubble>
      <MessageFooter>Delivered</MessageFooter>
    </MessageContent>
  </Message>
  <Message>
    <MessageAvatar>…</MessageAvatar>
    <MessageContent>
      <BubbleGroup>
        <Bubble variant="muted"><BubbleContent>It's always a one-line change 😭.</BubbleContent></Bubble>
        <Bubble variant="muted">
          <BubbleContent>Alright, let me take a look.</BubbleContent>
          <BubbleReactions><span role="img" aria-label="Thumbs up">👍</span></BubbleReactions>
        </Bubble>
      </BubbleGroup>
    </MessageContent>
  </Message>
  <Marker role="status">
    <MarkerContent className="shimmer"><span className="font-medium">Oliver</span> is typing…</MarkerContent>
  </Marker>
</div>

Message represents one turn of a conversation (chat). It renders nothing by itself: it is an alignment controller (align="start" or "end") that lays its children side by side with flex and reverses the order for sent messages (align="end" → flex-row-reverse).

A Message is typically made of two sibling elements:

  • MessageAvatar: the round avatar that identifies the author. Contains an <Avatar> with AvatarImage + AvatarFallback. Optional: without an avatar, the bubble takes the full width.
  • MessageContent: the vertical column that stacks MessageHeader (name / timestamp), the Bubble with its BubbleContent, and MessageFooter (status, inline actions such as "Delivered" or "Undo"/"Retry" buttons).

Bubble is the colored rectangle of the message. Seven variants: default (primary accent, for the current user), secondary (a less saturated fill), muted (neutral gray, for the other participants), tinted (light primary tint), outline (neutral background + border), ghost (no styling, for long assistant notes), destructive (red, for errors / warnings). BubbleContent is the actual text container; BubbleReactions is absolutely positioned on top (emojis / reactions).

To stack several bubbles from the same author, wrap them in a BubbleGroup inside MessageContent: a single avatar is shown for the whole group.

To show a system state (bot thinking, loading, status note), use Marker in place of MessageContent: <Marker role="status"><MarkerIcon><Spinner /></MarkerIcon><MarkerContent>Checking the logs...</MarkerContent></Marker>.

Installation

Terminal
$ bext ui add message

Usage

tsx
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageGroup,
  MessageHeader,
} from "@bext-stack/ui/primitives"
import { Avatar, AvatarFallback, AvatarImage } from "@bext-stack/ui/primitives"
import { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "@bext-stack/ui/primitives"

<Message align="end">
  <MessageAvatar>
    <Avatar>
      <AvatarImage src="/avatars/10.png" alt="@me" />
      <AvatarFallback>ME</AvatarFallback>
    </Avatar>
  </MessageAvatar>
  <MessageContent>
    <Bubble>
      <BubbleContent>Hello world.</BubbleContent>
    </Bubble>
  </MessageContent>
</Message>

Anatomy

<Message align="start | end">
  ├── <MessageAvatar>              // optional: the round avatar
  │   └── <Avatar>...</Avatar>
  └── <MessageContent>             // vertical column
      ├── <MessageHeader>...       // name + timestamp (optional)
      ├── <Bubble variant="...">   // the bubble, styled by variant
      │   ├── <BubbleContent>      // the message text
      │   └── <BubbleReactions>    // emoji reactions (optional)
      └── <MessageFooter>...       // status + actions (optional)

<MessageGroup>                     // wrapper that stacks several Messages from the same author
  ├── <Message>...</Message>
  └── <Message>...</Message>

API Reference

Message

Prop Type Default Description
align "start" | "end" "start" Alignment of the message. "end" triggers flex-row-reverse (avatar on the right, bubble flush right), typically for sent messages.
className string — Additional CSS classes on the root <div data-slot="message">.
children ReactNode — Children: typically MessageAvatar + MessageContent.

MessageGroup

Prop Type Default Description
className string — Wrapper that stacks several <Message> from the same author with gap-2.

MessageAvatar

Prop Type Default Description
className string — Avatar container. min-w-8 shrink-0 self-end + translate-y-8 when a MessageFooter is present (to stay aligned with the bubble).
children ReactNode — Typically an <Avatar> with AvatarImage + AvatarFallback.

MessageContent

Prop Type Default Description
className string — Vertical column that stacks Header + Bubble + Footer with gap-2.5.

MessageHeader

Prop Type Default Description
className string — Row above the bubble (author name, timestamp). text-xs muted-foreground.

MessageFooter

Prop Type Default Description
className string — Row below the bubble ("Delivered" status, "Undo"/"Retry" actions). Right-aligned when align="end".

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 "message" | "message-group" | "message-avatar" | "message-content" | "message-header" | "message-footer" Stable selector for custom styling.
data-align Message "start" | "end" Reflects the align prop. Used by MessageFooter for justify-end when align=end.

Keyboard

Key Action
Tab Focus next interactive element (avatar link, footer button).
Enter / Space Activate button/link in header/footer.

Accessibility

The Message component has no specific ARIA role: it is a visual container. Its children (Avatar, MessageFooter buttons) carry their own aria-label and roles where needed.

For a message thread, wrapping it in a container with role="log" and aria-live="polite" lets screen readers announce new messages without interrupting. MessageScroller gives you the scrollable thread container, but it sets neither attribute and does not forward them: put role="log" and aria-live="polite" on your own wrapper around it.

BubbleReactions does not forward aria-* props, so name each reaction on the child you pass in: <span role="img" aria-label="Thumbs up">👍</span> for a static emoji, an aria-label on the <button> for an icon-only reaction button. The emoji alone is not enough.

The Marker for system states ("Checking the logs…" with a spinner) has no role by default: pass role="status" so that assistive technologies announce it as a non-urgent state change.

Examples

Avatar

@shadcnCN
Something went wrong. Any idea?
@evilrabbitER
I checked the latest deployment and found the build failed during dependency installation.
@evilrabbitER
Something went wrong. Any idea?
@shadcnCN
I checked the latest deployment and found the build failed during dependency installation.
Can you share the exact error message from the logs?
CN
Something went wrong. Any idea?
tsx
<div className="flex w-full max-w-md flex-col gap-10">
  <Message>
    <MessageAvatar>
      <Avatar>
        <AvatarImage src="/avatars/01.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble variant="muted"><BubbleContent>Something went wrong. Any idea?</BubbleContent></Bubble>
    </MessageContent>
  </Message>
  <Message>
    <MessageAvatar>
      <Avatar><AvatarImage src="/avatars/02.png" alt="@evilrabbit" /><AvatarFallback>ER</AvatarFallback></Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble variant="muted">
        <BubbleContent>
          I checked the latest deployment and found the build failed during dependency installation.
        </BubbleContent>
      </Bubble>
    </MessageContent>
  </Message>
  <Message align="end">
    <MessageAvatar>
      <Avatar><AvatarImage src="/avatars/02.png" alt="@evilrabbit" /><AvatarFallback>ER</AvatarFallback></Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble variant="muted"><BubbleContent>Something went wrong. Any idea?</BubbleContent></Bubble>
    </MessageContent>
  </Message>
  <Message>
    <MessageAvatar>
      <Avatar><AvatarImage src="/avatars/03.png" alt="@shadcn" /><AvatarFallback>CN</AvatarFallback></Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble variant="muted">
        <BubbleContent>
          <div className="space-y-2">
            <div>I checked the latest deployment and found the build failed during dependency installation.</div>
            <div>Can you share the exact error message from the logs?</div>
          </div>
        </BubbleContent>
      </Bubble>
    </MessageContent>
  </Message>
  <Message>
    <MessageAvatar><Avatar><AvatarFallback>CN</AvatarFallback></Avatar></MessageAvatar>
    <MessageContent>
      <Bubble variant="destructive"><BubbleContent>Something went wrong. Any idea?</BubbleContent></Bubble>
    </MessageContent>
  </Message>
</div>

Group

I checked the registry addresses.
The component and example JSON now live under the UI registry.
I checked the registry addresses.
The component and example JSON now live under the UI registry.
I checked the registry addresses.
@shadcnCN
The component and example JSON now live under the UI registry.
I checked the registry addresses.
@shadcnCN
The component and example JSON now live under the UI registry.
tsx
// One of the four groups shown above: the avatar sits on the last message only.
<MessageGroup>
  <Message>
    <MessageAvatar />
    <MessageContent>
      <Bubble variant="muted"><BubbleContent>I checked the registry addresses.</BubbleContent></Bubble>
    </MessageContent>
  </Message>
  <Message>
    <MessageAvatar>
      <Avatar><AvatarImage src="/avatars/03.png" alt="@shadcn" /><AvatarFallback>CN</AvatarFallback></Avatar>
    </MessageAvatar>
    <MessageContent>
      <Bubble variant="muted"><BubbleContent>The component and example JSON now live under the UI registry.</BubbleContent></Bubble>
    </MessageContent>
  </Message>
</MessageGroup>

Group Chat

Why is the dashboard showing zero users
Define "zero."
@evilrabbitER
What do you mean?
@eve can you check?
Checking the logs…
Eve
I found the following error in the logs: - Error: No users found - Stack trace: - at src/users/services/user-service.ts:123 - at src/users/services/get-user.ts:230 Can you check if the users are being created?
tsx
// Shortened: the first message, the status Marker and the ghost reply.
<Message align="end">
  <MessageContent>
    <Bubble variant="tinted"><BubbleContent>Why is the dashboard showing zero users</BubbleContent></Bubble>
  </MessageContent>
</Message>
<Message>
  <Marker role="status">
    <MarkerIcon><Spinner /></MarkerIcon>
    <MarkerContent className="shimmer">Checking the logs…</MarkerContent>
  </Marker>
</Message>
<Message>
  <MessageContent>
    <MessageHeader>Eve</MessageHeader>
    <Bubble variant="ghost">
      <BubbleContent>...</BubbleContent>
    </Bubble>
  </MessageContent>
</Message>
You
Can we keep this style quiet and send the update today?
Delivered
Olivia1m ago
I checked the logs. The retry finished and the missing invoices are included now.
From Support queue
Marina
I drafted the reply with the exact filter path and a link to the export history.
Draft ready
Assistant note
Ghost messages keep long-form assistant notes aligned with the surrounding text instead of adding a frame.
Internal only
Automation warning
The old export link expires in 10 minutes. Regenerate before sending this transcript externally.
Needs action
tsx
// Shortened: the first of the five messages. Button has no "xs" size, hence the classes.
<Message align="end">
  <MessageContent>
    <MessageHeader className="justify-end">You</MessageHeader>
    <Bubble><BubbleContent>Can we keep this style quiet and send the update today?</BubbleContent></Bubble>
    <MessageFooter className="gap-2">
      <span>Delivered</span>
      <Button variant="ghost" className="h-6 px-2 text-xs">Undo</Button>
    </MessageFooter>
  </MessageContent>
</Message>

Actions

What is the status of the deployment?
@evilrabbitER
The install failure is coming from the workspace package. I can open the exact log line or retry the deployment.
Okay drop me a link. I'll check it out.
Failed to send
tsx
<Message align="end">
  <MessageContent>
    <Bubble><BubbleContent>What is the status of the deployment?</BubbleContent></Bubble>
    <MessageFooter>
      <Button variant="ghost" size="icon" aria-label="Copy"><CopyIcon /></Button>
      <Button variant="ghost" size="icon" aria-label="Regenerate"><RefreshCcwIcon /></Button>
    </MessageFooter>
  </MessageContent>
</Message>

Attachment

Workspace
Here's the image. Can you add it to the PDF? Use it for the cover page.
Done. Here's the PDF with the image added as the cover page.
sales-dashboard.pdfPDF · 2.4 MB
Thanks. Looks good.
tsx
<Message align="end">
  <MessageContent>
    <Attachment orientation="vertical">
      <AttachmentMedia variant="image"><img src="/shadcn-images/dashboard-1.jpg" alt="Workspace" /></AttachmentMedia>
    </Attachment>
    <Bubble>
      <BubbleContent>Here's the image. Can you add it to the PDF? Use it for the cover page.</BubbleContent>
    </Bubble>
  </MessageContent>
</Message>

Attachment Group

Photo 1
Photo 2
Photo 3
Here are the brand photos. Can you add them to the deck?
Done. Here's the draft deck and the exported assets.
brand-deck.pdfPDF · 3.1 MB
brand-deck.keyKeynote · 9 MB
brand-assets.zipZIP · 4.2 MB
tsx
<Message align="end">
  <MessageContent>
    <AttachmentGroup className="w-[80%]">
      {photos.map(url => (
        <Attachment orientation="vertical" key={url}>
          <AttachmentMedia variant="image"><img src={url} alt="Photo" /></AttachmentMedia>
        </Attachment>
      ))}
    </AttachmentGroup>
    <Bubble>
      <BubbleContent>Here are the brand photos. Can you add them to the deck?</BubbleContent>
    </Bubble>
  </MessageContent>
</Message>