Lay out a message with its avatar, alignment, header, and footer.
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 messageUsage
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
└── MessageFooterUse 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.
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.
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.
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> )}Header and Footer
Use MessageHeader for the sender name and MessageFooter for delivery or read status. Both sit flush without horizontal padding.
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.
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.
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.
| Prop | Type | Default |
|---|---|---|
| align | "start" | "end" | "start" |
| className | string | — |
<Message align="end"> <MessageAvatar /> <MessageContent /></Message>MessageGroup
Groups consecutive messages from the same sender.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<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.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<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.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<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.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<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.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
<MessageFooter>Delivered</MessageFooter>