Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Chat

Message

A conversation row with avatar, header, and footer slots that positions a message on either side.

Playground

OW
Olivia Wong
The install failure is coming from the workspace package.
Read · 10:42
—align
—variant
—MessageAvatar
—MessageHeader
—MessageFooter
—dir
Usage
Message owns the row: it lays out the avatar next to the content and flips the order when align is "end". The visible surface is delegated — drop a Bubble inside MessageContent and the bubble decides the color, radius, and max width.
CN
How can I help you today?
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>
Alignment
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.
OW
The deploy is queued behind the migration.
ME
Thanks — I'll wait it out.
<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>
Grouping
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.
I already checked the logs.
Nothing unusual before the crash.
OW
Want me to re-run it with tracing on?
<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>
Header and footer
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.
Olivia
I already checked the logs.
Send the report to the team.
Read Yesterday
<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>
Actions
The footer is a plain flex row, so it also holds controls — copy, retry, or feedback on an assistant reply. Use ghost icon buttons and pull them back with a negative inline start margin so the icons optically line up with the bubble text.
The install failure is from the workspace package — its peers were never installed.
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>
Attachments
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.
CN
Done. Here's the PDF with the image added.
sales-dashboard.pdfPDF · 2.4 MB
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>
Accessibility
Message only provides layout — the semantics come from what you put inside it. Give every icon-only footer control an 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>
API Reference
Six unstyled-by-default 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.
PartPropDefault
Messagealign?: "start" | "end""start"
MessageclassName?: string—
MessageGroupclassName?: string—
MessageAvatarclassName?: string—
MessageContentclassName?: string—
MessageHeaderclassName?: string—
MessageFooterclassName?: string—

Tokens used

6 design tokens consumed by the Message component.

  • --muted

    Muted

    bg-muted
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --text-sm

    Text Small (14px)

    text-sm
  • --text-xs

    Text Extra Small (12px)

    text-xs
  • --font-weight-medium

    Font Weight Medium (500)

    font-medium
  • --spacing

    Spacing unit (4px)

    gap-2gap-2.5px-3min-w-8-translate-y-8