Message

Conversation row with avatars, metadata, and alignment.

Lay out a message with its avatar, alignment, header, and footer.

Mina ChoMC
Deploying to prod real quick.
Jordan BlakeJB
It's 4:55 PM. On a Friday.
Mina ChoMC
It's a one-line change.
Delivered
Jordan BlakeJB
It's always a one-line change.
Alright, let me take a look.
👍
Oliver is typing…
tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"import { Bubble, BubbleContent, BubbleReactions } from "@/components/ui/bubble"import { Marker, MarkerContent } from "@/components/ui/marker"import {  Message,  MessageAvatar,  MessageContent,  MessageFooter,} from "@/components/ui/message"export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <Message align="end">        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-1.png" alt="Mina Cho" />            <AvatarFallback>MC</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="default" align="end">            <BubbleContent>Deploying to prod real quick.</BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message>        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-2.png" alt="Jordan Blake" />            <AvatarFallback>JB</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>It's 4:55 PM. On a Friday.</BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message align="end">        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-1.png" alt="Mina Cho" />            <AvatarFallback>MC</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="default" align="end">            <BubbleContent>It's a one-line change.</BubbleContent>          </Bubble>          <MessageFooter>Delivered</MessageFooter>        </MessageContent>      </Message>      <Message>        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-2.png" alt="Jordan Blake" />            <AvatarFallback>JB</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>It's always a one-line change.</BubbleContent>          </Bubble>          <Bubble variant="secondary">            <BubbleContent>Alright, let me take a look.</BubbleContent>            <BubbleReactions>👍</BubbleReactions>          </Bubble>        </MessageContent>      </Message>      <Marker role="status">        <MarkerContent className="shimmer">Oliver is typing…</MarkerContent>      </Marker>    </div>  )}

Message handles the row layout. Use Bubble for the visible surface and Avatar for portraits.

Installation

npx @arctis-sh/@arctis-sh/ui@latest add message

Usage

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"import { Bubble, BubbleContent } from "@/components/ui/bubble"import { Message, MessageAvatar, MessageContent } from "@/components/ui/message"
<Message>  <MessageAvatar>    <Avatar size="sm">      <AvatarImage src="/assets/brand/demos/avatars/avatar-1.png" alt="Mina Cho" />      <AvatarFallback>MC</AvatarFallback>    </Avatar>  </MessageAvatar>  <MessageContent>    <Bubble>      <BubbleContent>How can I help you today?</BubbleContent>    </Bubble>  </MessageContent></Message>

Composition

Message
├── MessageAvatar
└── MessageContent
    ├── MessageHeader
    ├── Bubble
    └── MessageFooter

Use MessageGroup to stack consecutive messages from one sender:

MessageGroup
├── Message
└── Message

Basic

A short thread with start and end alignment, footer status, reactions, and a typing Marker.

Mina ChoMC
Deploying to prod real quick.
Jordan BlakeJB
It's 4:55 PM. On a Friday.
Mina ChoMC
It's a one-line change.
Delivered
Jordan BlakeJB
It's always a one-line change.
Alright, let me take a look.
👍
Oliver is typing…
tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"import { Bubble, BubbleContent, BubbleReactions } from "@/components/ui/bubble"import { Marker, MarkerContent } from "@/components/ui/marker"import {  Message,  MessageAvatar,  MessageContent,  MessageFooter,} from "@/components/ui/message"export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <Message align="end">        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-1.png" alt="Mina Cho" />            <AvatarFallback>MC</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="default" align="end">            <BubbleContent>Deploying to prod real quick.</BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message>        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-2.png" alt="Jordan Blake" />            <AvatarFallback>JB</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>It's 4:55 PM. On a Friday.</BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message align="end">        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-1.png" alt="Mina Cho" />            <AvatarFallback>MC</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="default" align="end">            <BubbleContent>It's a one-line change.</BubbleContent>          </Bubble>          <MessageFooter>Delivered</MessageFooter>        </MessageContent>      </Message>      <Message>        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-2.png" alt="Jordan Blake" />            <AvatarFallback>JB</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>It's always a one-line change.</BubbleContent>          </Bubble>          <Bubble variant="secondary">            <BubbleContent>Alright, let me take a look.</BubbleContent>            <BubbleReactions>👍</BubbleReactions>          </Bubble>        </MessageContent>      </Message>      <Marker role="status">        <MarkerContent className="shimmer">Oliver is typing…</MarkerContent>      </Marker>    </div>  )}

Avatar

Place MessageAvatar beside the message. align="end" flips the row. The avatar centers on the bubble’s first line of text, while empty avatar slots keep grouped rows aligned.

The build failed during dependency installation.
Can you share the exact error?
Jordan BlakeJB
Here's the error from the logs
tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"import { Bubble, BubbleContent } from "@/components/ui/bubble"import {  Message,  MessageAvatar,  MessageContent,  MessageGroup,} from "@/components/ui/message"export function Example() {  return (    <MessageGroup>      <Message>        <MessageAvatar />        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>The build failed during dependency installation.</BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message>        <MessageAvatar />        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>Can you share the exact error?</BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message>        <MessageAvatar>          <Avatar size="sm">            <AvatarImage src="/assets/brand/demos/avatars/avatar-2.png" alt="Jordan Blake" />            <AvatarFallback>JB</AvatarFallback>          </Avatar>        </MessageAvatar>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>Here's the error from the logs</BubbleContent>          </Bubble>        </MessageContent>      </Message>    </MessageGroup>  )}

| align | Description | | --- | --- | | start | Align the message to the start of the conversation. | | end | Align the message to the end of the conversation. |

Group

Use MessageGroup for consecutive messages from one sender. Leave earlier MessageAvatar slots empty so each row lines up with the final avatar.

I checked the registry addresses.
Priya NairPN
The component and example JSON now live under the UI registry.
tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"import { Bubble, BubbleContent } from "@/components/ui/bubble"import {  Message,  MessageAvatar,  MessageContent,  MessageGroup,} from "@/components/ui/message"export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <MessageGroup>        <Message>          <MessageAvatar />          <MessageContent>            <Bubble variant="secondary">              <BubbleContent>I checked the registry addresses.</BubbleContent>            </Bubble>          </MessageContent>        </Message>        <Message>          <MessageAvatar>            <Avatar size="sm">              <AvatarImage src="/assets/brand/demos/avatars/avatar-3.png" alt="Priya Nair" />              <AvatarFallback>PN</AvatarFallback>            </Avatar>          </MessageAvatar>          <MessageContent>            <Bubble variant="secondary">              <BubbleContent>                The component and example JSON now live under the UI registry.              </BubbleContent>            </Bubble>          </MessageContent>        </Message>      </MessageGroup>    </div>  )}

Use MessageHeader for the sender name and MessageFooter for delivery or read status. Both sit flush without horizontal padding.

Olivia
I already checked the logs.
Send the report to the team. Ping @team if you need help.
Read Yesterday
tsx
import { Bubble, BubbleContent } from "@/components/ui/bubble"import {  Message,  MessageContent,  MessageFooter,  MessageHeader,} from "@/components/ui/message"export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <Message>        <MessageContent>          <MessageHeader>Olivia</MessageHeader>          <Bubble variant="secondary">            <BubbleContent>I already checked the logs.</BubbleContent>          </Bubble>          <Bubble variant="secondary">            <BubbleContent>              Send the report to the team. Ping @team if you need help.            </BubbleContent>          </Bubble>          <MessageFooter>Read Yesterday</MessageFooter>        </MessageContent>      </Message>    </div>  )}
<Message>  <MessageContent>    <MessageHeader>Olivia</MessageHeader>    <Bubble variant="secondary">      <BubbleContent>I already checked the logs.</BubbleContent>    </Bubble>    <MessageFooter>Read yesterday</MessageFooter>  </MessageContent></Message>

Actions

Place copy, retry, or feedback controls in MessageFooter. Give icon-only buttons an aria-label.

The install failure is coming from the workspace package.
Okay drop me a link. Taking a look…
Failed to send
tsx
import { Bubble, BubbleContent } from "@/components/ui/bubble"import { Button } from "@/components/ui/button"import {  Message,  MessageContent,  MessageFooter,} from "@/components/ui/message"function CopyIcon({ className }: { className?: string }) {  return (    <svg      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      aria-hidden="true"      className={className}    >      <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />      <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />    </svg>  )}function RefreshIcon({ className }: { className?: string }) {  return (    <svg      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      aria-hidden="true"      className={className}    >      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />      <path d="M21 3v5h-5" />      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />      <path d="M8 16H3v5" />    </svg>  )}export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <Message>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>              The install failure is coming from the workspace package.            </BubbleContent>          </Bubble>          <Bubble variant="secondary">            <BubbleContent>Okay drop me a link. Taking a look…</BubbleContent>          </Bubble>          <MessageFooter className="gap-1">            <Button variant="ghost" size="icon-xs" aria-label="Copy">              <CopyIcon />            </Button>            <Button variant="ghost" size="icon-xs" aria-label="Retry">              <RefreshIcon />            </Button>            <span className="ml-1 text-destructive">Failed to send</span>          </MessageFooter>        </MessageContent>      </Message>    </div>  )}
<MessageFooter className="gap-1">  <Button variant="ghost" size="icon-xs" aria-label="Copy">    <CopyIcon />  </Button>  <Button variant="ghost" size="icon-xs" aria-label="Retry">    <RefreshIcon />  </Button></MessageFooter>

Attachment

Place Attachment below the bubble for files and downloads.

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.pdf
PDF · 2.4 MB
Thanks. Looks good.
tsx
import {  Attachment,  AttachmentAction,  AttachmentActions,  AttachmentContent,  AttachmentDescription,  AttachmentMedia,  AttachmentTitle,} from "@/components/ui/attachment"import { Bubble, BubbleContent } from "@/components/ui/bubble"import { Message, MessageContent } from "@/components/ui/message"function FileTextIcon({ className }: { className?: string }) {  return (    <svg      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      aria-hidden="true"      className={className}    >      <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />      <path d="M14 2v4a2 2 0 0 0 2 2h4" />      <path d="M10 9H8" />      <path d="M16 13H8" />      <path d="M16 17H8" />    </svg>  )}function DownloadIcon({ className }: { className?: string }) {  return (    <svg      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      aria-hidden="true"      className={className}    >      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />      <polyline points="7 10 12 15 17 10" />      <line x1="12" x2="12" y1="15" y2="3" />    </svg>  )}export function Example() {  return (    <div className="flex w-full flex-col gap-3">      <Message>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>              Here's the image. Can you add it to the PDF? Use it for the cover page.            </BubbleContent>          </Bubble>        </MessageContent>      </Message>      <Message align="end">        <MessageContent>          <Bubble variant="default" align="end">            <BubbleContent>              Done. Here's the PDF with the image added as the cover page.            </BubbleContent>          </Bubble>          <Attachment className="max-w-xs">            <AttachmentMedia>              <FileTextIcon />            </AttachmentMedia>            <AttachmentContent>              <AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>              <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>            </AttachmentContent>            <AttachmentActions>              <AttachmentAction aria-label="Download sales-dashboard.pdf">                <DownloadIcon />              </AttachmentAction>            </AttachmentActions>          </Attachment>        </MessageContent>      </Message>      <Message>        <MessageContent>          <Bubble variant="secondary">            <BubbleContent>Thanks. Looks good.</BubbleContent>          </Bubble>        </MessageContent>      </Message>    </div>  )}

Accessibility

Message is presentational, so its accessibility comes from the content inside it.

Icon-only actions — give each footer control an aria-label.

<MessageFooter>  <Button variant="ghost" size="icon-xs" aria-label="Copy">    <CopyIcon />  </Button></MessageFooter>

Status updates — for in-progress rows, use Marker with role="status".

<Message>  <Marker role="status">    <MarkerIcon>      <Spinner />    </MarkerIcon>    <MarkerContent>Checking the logs…</MarkerContent>  </Marker></Message>

API Reference

Message

Wrapper for a message row. Standard HTML attributes pass through to the root.

PropTypeDefault
align"start" | "end""start"
classNamestring
<Message align="end">  <MessageAvatar />  <MessageContent /></Message>

MessageGroup

Groups consecutive messages from the same sender.

PropTypeDefault
classNamestring
<MessageGroup>  <Message>...</Message>  <Message>...</Message></MessageGroup>

MessageAvatar

Avatar slot. Vertically centered on the bubble’s first text line — not the full multi-line height. Accepts native div props.

PropTypeDefault
classNamestring
<MessageAvatar>  <Avatar size="sm">    <AvatarImage src="/assets/brand/demos/avatars/avatar-1.png" alt="Mina Cho" />    <AvatarFallback>MC</AvatarFallback>  </Avatar></MessageAvatar>

MessageContent

Wraps the header, message surface, and footer.

PropTypeDefault
classNamestring
<MessageContent>  <MessageHeader>Olivia</MessageHeader>  <Bubble>    <BubbleContent>How can I help you today?</BubbleContent>  </Bubble>  <MessageFooter>Delivered</MessageFooter></MessageContent>

MessageHeader

Content above the message, such as a sender name. Flush start, no horizontal padding. Stays start-aligned regardless of align.

PropTypeDefault
classNamestring
<MessageHeader>Olivia</MessageHeader>

MessageFooter

Content below the message, such as status or actions. Flush to the message side, no horizontal padding. Follows align on the row.

PropTypeDefault
classNamestring
<MessageFooter>Delivered</MessageFooter>