Message
Renders a single chat message — role-aware bubble, markdown, chips, attachments, sources, and actions.
Use flexbox on the parent: display: flex, then justify-content: center and align-items: center.
Usage guidelines
- One turn's content — a role-aware bubble composed from plain message fields (role, text, attachments, sources).
- Rich text —
Message.Markdownfor assistant markdown;Message.Textreconstructs inline chips from the wire format. - Affordances — copy, regenerate and other actions, source pills, timestamps, and error / stopped / loading markers.
- Selection → chat —
Message.Selectionlifts highlighted text back into the composer. - Get started — see Installation to add the package and copy the component.
Anatomy
The bare nesting — Message and Message.Content carry the structure:
<Message role={role} isLast={isLast} isError={isError}>
<Message.Content>
<Message.Text>{text}</Message.Text>
</Message.Content>
<Message.Actions>
<Message.Copy value={text} />
</Message.Actions>
</Message>A realistic assistant turn with markdown, sources, and actions:
<Message.Turn sticky>
<Message role="assistant" isLast isError={false}>
<Message.Content>
<Message.Markdown>{content}</Message.Markdown>
<Message.Sources>
{sources.map((s) => (
<Message.Source key={s.url} url={s.url} domain={s.domain} />
))}
</Message.Sources>
</Message.Content>
<Message.Actions>
<Message.Copy value={content} />
<Message.Action tooltip="Regenerate" onClick={regenerate}>
<RefreshIcon />
</Message.Action>
</Message.Actions>
</Message>
</Message.Turn>Performance
Message is compositional — you pass its parts as children — which means the
package cannot memoize rows for you: a parent re-render re-creates the children
elements, so a memo inside Message would compare fresh trees and never
bail. The memo boundary has to be your row component, the one that receives
the message object and derives everything inside:
const ChatMessageItem = memo(({ message, isLast, isStreaming }: ChatMessageItemProps) => {
const { parts } = message;
// segmentation, part mapping, actions — all derived in here
return <Message role={message.role} isLast={isLast}>{/* … */}</Message>;
});
{messages.map((message) => (
<ChatMessageItem
key={message.id}
message={message}
isLast={message.id === lastMessageId}
isStreaming={message.id === lastMessageId && isStreaming}
/>
))}Three rules keep the memo effective while a reply streams:
- Pass the original message object. Finished messages keep reference
identity across stream chunks; spreading (
{ parts, ...message }) mints a fresh object every render and silently defeats the memo. - Make flags per-message.
isStreamingshould mean this message is streaming — passing the chat-wide status re-renders every row on each status transition. - Take callbacks from stable context inside the row, not as inline props from the map.
Done right, a stream chunk re-renders exactly one row. See Streaming performance for the full render model.
Accessibility
- Role drives
data-role, and error/last state drivedata-error/data-laston the root, so styling and assistive context stay in sync. - Speaker identity is yours to announce.
roleis an opaque string the package only surfaces asdata-role— alignment and color are invisible to assistive tech. Give each message a visually-hidden{role} saidprefix, or anaria-labelon the root, so a transcript read top-to-bottom attributes its turns. - Error / stopped / loading markers are consumer-owned slots: give an
inline failure marker
role="alert"so it announces immediately, and quieter states ("stopped", "generating…")role="status". - Sources render as
<a target="_blank" rel="noopener noreferrer">. - Actions are real buttons;
Message.Actionpairs with a tooltip label.
API reference
Every part accepts className and style and emits a bespoke part attribute unless
noted. The headless-backed parts — Message, Message.Turn, Message.Text —
also accept render (see PrimitiveProps); the rest are
consumer-owned styled parts you edit directly.
Message
The root: role-aware container. Renders data-message.
| Prop | Type | Default |
|---|---|---|
role | string | (required) |
isLast | boolean | false |
isError | boolean | false |
| Attribute | Values | Description |
|---|---|---|
data-message | — | The message root. |
data-role | string | The message role you passed (commonly system / user / assistant). |
data-error | — | Present when isError is true. |
data-last | — | Present when isLast is true. |
Message.Turn
Groups a role's messages into one visual turn. Renders data-message-turn.
| Prop | Type | Default |
|---|---|---|
sticky | boolean | — |
Message.Text
Plain/segmented text: reconstructs inline chips from the wire format. Renders
data-message-text.
| Prop | Type | Default |
|---|---|---|
children | string | (required) |
renderText | (text, index) => ReactNode | — |
renderChip | (chip, index) => ReactNode | — |
Message.Markdown
Renders assistant markdown (via the shared Markdown renderer).
| Prop | Type | Default |
|---|---|---|
children | string | (required) |
Message.Copy
Copy-to-clipboard action with a copied state.
| Prop | Type | Default |
|---|---|---|
value | string | (required) |
Message.Action
An action button with a tooltip (styled IconButton).
| Prop | Type | Default |
|---|---|---|
tooltip | string | — |
Message.Source
A source pill linking out. Renders data-message-source as
<a target="_blank" rel="noopener noreferrer">.
| Prop | Type | Default |
|---|---|---|
url | string | (required) |
domain | string | — |
Message.Attachment
A read-only attachment chip (image hover-card or file icon).
| Prop | Type | Default |
|---|---|---|
attachment | { url; mediaType?; filename? } | (required) |
Message.Selection
A floating toolbar over a text selection within this message.
| Prop | Type | Default |
|---|---|---|
onAdd | (text: string) => void | (required) |
Structural parts
Message.Content, Message.Actions, Message.Attachments, Message.Sources,
Message.Error, Message.Stopped, and Message.Loading are consumer-owned
layout/marker slots taking only children; they render
data-message-content, "message-actions", "message-attachments",
"message-sources", "message-error", "message-stopped", and
"message-loading" respectively — plain styled elements in your copy of the
component, not package primitives.
Hooks
| Prop | Type | Default |
|---|---|---|
useMessageSelection | (scope) => MessageSelection | null | — |
useMessageSelectionScope | () => { anchorRef, contentElement } | — |