Chat
A conversation row with avatar, header, and footer slots that positions a message on either side.
Playground
align is "end". The visible surface is delegated — drop a Bubble inside MessageContent and the bubble decides the color, radius, and max width.import { Avatar, AvatarFallback } from "@workspace/ui/components/avatar"
import { Bubble, BubbleContent } from "@workspace/ui/components/bubble"
import {
Message,
MessageAvatar,
MessageContent,
} from "@workspace/ui/components/message"
<Message>
<MessageAvatar>
<Avatar className="size-8">
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble>
<BubbleContent>How can I help you today?</BubbleContent>
</Bubble>
</MessageContent>
</Message>align="end" reverses the row so the avatar sits after the content and every child of MessageContent aligns to the end — the incoming/outgoing split of a chat thread. Alignment uses logical properties, so the sides mirror under dir="rtl" without extra work.<Message>
<MessageAvatar>
<Avatar className="size-8">
<AvatarFallback>OW</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble variant="muted">
<BubbleContent>The deploy is queued behind the migration.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message align="end">
<MessageAvatar>
<Avatar className="size-8">
<AvatarFallback>ME</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble>
<BubbleContent>Thanks — I'll wait it out.</BubbleContent>
</Bubble>
</MessageContent>
</Message>MessageGroup stacks consecutive messages from the same sender with a tighter gap. Render an empty MessageAvatar on the earlier messages: it keeps the gutter reserved so the bubbles stay in one column, and only the last message carries the actual avatar.<MessageGroup>
<Message>
<MessageAvatar />
<MessageContent>
<Bubble variant="muted">
<BubbleContent>I already checked the logs.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageAvatar />
<MessageContent>
<Bubble variant="muted">
<BubbleContent>Nothing unusual before the crash.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageAvatar>
<Avatar className="size-8">
<AvatarFallback>OW</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble variant="muted">
<BubbleContent>Want me to re-run it with tracing on?</BubbleContent>
</Bubble>
</MessageContent>
</Message>
</MessageGroup>MessageHeader names the sender above the bubble; MessageFooter carries delivery status, timestamps, or actions below it. Both render in the muted foreground at text-xs and inherit the row alignment — the footer justifies to the end on an align="end" message. When a footer is present the avatar lifts to stay level with the bubble.<Message>
<MessageContent>
<MessageHeader>Olivia</MessageHeader>
<Bubble variant="muted">
<BubbleContent>I already checked the logs.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message align="end">
<MessageContent>
<Bubble>
<BubbleContent>Send the report to the team.</BubbleContent>
</Bubble>
<MessageFooter>
<div>
Read <span className="font-normal">Yesterday</span>
</div>
</MessageFooter>
</MessageContent>
</Message>import { CopyIcon, ThumbsDownIcon, ThumbsUpIcon } from "@phosphor-icons/react/dist/ssr"
<Message>
<MessageContent>
<Bubble variant="muted">
<BubbleContent>
The install failure is from the workspace package — its peers were never
installed.
</BubbleContent>
</Bubble>
<MessageFooter className="-ms-1 gap-0.5">
<Button variant="ghost" size="icon-sm" aria-label="Copy">
<CopyIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Like">
<ThumbsUpIcon />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Dislike">
<ThumbsDownIcon />
</Button>
</MessageFooter>
</MessageContent>
</Message>MessageContent is a column, so anything placed after the bubble stacks under it and follows the row alignment. Add an Attachment to ship a file alongside the text.import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@workspace/ui/components/attachment"
<Message>
<MessageAvatar>
<Avatar className="size-8">
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble variant="muted">
<BubbleContent>Done. Here's the PDF with the image added.</BubbleContent>
</Bubble>
<Attachment>
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction
type="button"
title="Download"
aria-label="Download"
size="icon-sm"
variant="secondary"
>
<DownloadSimpleIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</MessageContent>
</Message>aria-label, since the icon alone announces nothing. For in-progress updates inside a thread, use a Marker with role="status" so screen readers hear the change politely instead of on every re-render. A transcript that grows over time belongs in a MessageScroller, which owns the live region and scroll behaviour.<MessageFooter>
<Button variant="ghost" size="icon-sm" aria-label="Copy">
<CopyIcon />
</Button>
</MessageFooter>
<Message>
<Marker role="status">
<MarkerIcon>
<Spinner />
</MarkerIcon>
<MarkerContent>Checking the logs…</MarkerContent>
</Marker>
</Message>div wrappers. Only Message takes a prop of its own — align, mirrored to a data-align attribute that the other parts read to position themselves. Everything else is styled through className, and all native div props pass through.| Part | Prop | Default |
|---|---|---|
| Message | align?: "start" | "end" | "start" |
| Message | className?: string | — |
| MessageGroup | className?: string | — |
| MessageAvatar | className?: string | — |
| MessageContent | className?: string | — |
| MessageHeader | className?: string | — |
| MessageFooter | className?: string | — |
Tokens used
6 design tokens consumed by the Message component.
--muted
Muted
--muted-foreground
Muted Foreground
--text-sm
Text Small (14px)
--text-xs
Text Extra Small (12px)
--font-weight-medium
Font Weight Medium (500)
--spacing
Spacing unit (4px)