--- 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." }, ];