---
title: Message
description: Renders a single chat message — role-aware bubble, markdown, chips, attachments, sources, and actions.
source: message.tsx
---
## Usage guidelines
- **One turn's content** — a role-aware bubble composed from plain message fields (role, text, attachments, sources).
- **Rich text** — `Message.Markdown` for assistant markdown; `Message.Text` reconstructs inline chips from the wire format.
- **Affordances** — copy, regenerate and other actions, source pills, timestamps, and error / stopped / loading markers.
- **Selection → chat** — `Message.Selection` lifts highlighted text back into the composer.
- **Get started** — see [Installation](/docs/installation) to add the package and copy the component.
## Anatomy
The bare nesting — `Message` and `Message.Content` carry the structure:
```tsx
{text}
```
A realistic assistant turn with markdown, sources, and actions:
```tsx
{content}
{sources.map((s) => (
))}
```
## 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:
```tsx
const ChatMessageItem = memo(({ message, isLast, isStreaming }: ChatMessageItemProps) => {
const { parts } = message;
// segmentation, part mapping, actions — all derived in here
return {/* … */};
});
{messages.map((message) => (
))}
```
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.** `isStreaming` should 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](/docs/headless/performance) for the full render model.
## Accessibility
- **Role** drives `data-role`, and error/last state drive `data-error`/`data-last`
on the root, so styling and assistive context stay in sync.
- **Speaker identity is yours to announce.** `role` is an opaque string the
package only surfaces as `data-role` — alignment and color are invisible to
assistive tech. Give each message a visually-hidden `{role} said` prefix, or
an `aria-label` on 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 ``.
- **Actions** are real buttons; `Message.Action` pairs 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](/docs/headless/types)); the rest are
consumer-owned styled parts you edit directly.
### Message
The root: role-aware container. Renders `data-message`.
export const rootProps = [
{ name: "role", type: "string", default: "(required)", description: "Opaque role string surfaced as data-role; you own the set (commonly system / user / assistant)." },
{ name: "isLast", type: "boolean", default: "false", description: "Marks the last message (data-last) — a streaming/animation hook." },
{ name: "isError", type: "boolean", default: "false", description: "Renders the error surface (data-error)." },
];
export const rootAttrs = [
{ attribute: "data-message", description: "The message root." },
{ attribute: "data-role", values: "string", description: "The message role you passed (commonly system / user / assistant)." },
{ attribute: "data-error", description: "Present when isError is true." },
{ attribute: "data-last", description: "Present when isLast is true." },
];
### Message.Turn
Groups a role's messages into one visual turn. Renders `data-message-turn`.
export const turnProps = [
{ name: "sticky", type: "boolean", description: "Pins the turn header while its content scrolls." },
];
### Message.Text
Plain/segmented text: reconstructs inline chips from the wire format. Renders
`data-message-text`.
export const textProps = [
{ name: "children", type: "string", default: "(required)", description: "The message text; chip tokens are parsed out and rendered." },
{ name: "renderText", type: "(text, index) => ReactNode", description: "Custom renderer for plain text runs." },
{ name: "renderChip", type: "(chip, index) => ReactNode", description: "Custom renderer for reconstructed chips." },
];
### Message.Markdown
Renders assistant markdown (via the shared Markdown renderer).
export const markdownProps = [
{ name: "children", type: "string", default: "(required)", description: "Markdown source." },
];
### Message.Copy
Copy-to-clipboard action with a copied state.
export const copyProps = [
{ name: "value", type: "string", default: "(required)", description: "Text copied to the clipboard." },
];
### Message.Action
An action button with a tooltip (styled `IconButton`).
export const actionProps = [
{ name: "tooltip", type: "string", description: "Tooltip label; also the accessible name." },
];
### Message.Source
A source pill linking out. Renders `data-message-source` as
``.
export const sourceProps = [
{ name: "url", type: "string", default: "(required)", description: "The source URL." },
{ name: "domain", type: "string", description: "Display domain (with favicon)." },
];
### Message.Attachment
A read-only attachment chip (image hover-card or file icon).
export const attachmentProps = [
{ name: "attachment", type: "{ url; mediaType?; filename? }", default: "(required)", description: "The attachment to render — a minimal structural shape (an AI SDK FilePart satisfies it)." },
];
### Message.Selection
A floating toolbar over a text selection within this message.
export const toolbarProps = [
{ name: "onAdd", type: "(text: string) => void", default: "(required)", description: "Called with the selected text — e.g. to add it to the composer as context." },
];
### 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
export const hooks = [
{ name: "useMessageSelection", type: "(scope) => MessageSelection | null", description: "Subscribe to the text selection scoped to a message element (settles on mouseup/keyup)." },
{ name: "useMessageSelectionScope", type: "() => { anchorRef, contentElement }", description: "Resolve the owning message's content element from an anchor rendered inside it." },
];