Build custom message rendering with content parts, attachments, and hover state.
The Message primitive handles individual message rendering: content parts, attachments, quotes, hover state, and error display. It's the building block inside each message bubble, resolving text, images, tool calls, and more through a parts pipeline.
What is assistant-ui?
assistant-ui is a set of React components for building AI chat interfaces. It provides unstyled primitives that handle state management, streaming, and accessibility โ you bring the design.
import {
MessagePrimitive,
MessagePartPrimitive,
} from "@assistant-ui/react";
function UserMessage() {
return (
<MessagePrimitive.Root className="flex justify-end">
<div className="max-w-[80%] rounded-2xl bg-primary px-4 py-2.5 text-sm text-primary-foreground">
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <UserText />;
return null;
}}
</MessagePrimitive.Parts>
</div>
</MessagePrimitive.Root>
);
}
function AssistantMessage() {
return (
<MessagePrimitive.Root className="flex justify-start gap-3">
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
AI
</div>
<div className="max-w-[80%] rounded-2xl bg-muted px-4 py-2.5 text-sm">
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <AssistantText />;
return part.toolUI ?? null;
}}
</MessagePrimitive.Parts>
</div>
</MessagePrimitive.Root>
);
}
function UserText() {
return (
<p>
<MessagePartPrimitive.Text />
</p>
);
}
function AssistantText() {
return (
<p className="leading-relaxed">
<MessagePartPrimitive.Text />
</p>
);
}Quick Start
A minimal message with parts rendering:
import { MessagePrimitive } from "@assistant-ui/react";
<MessagePrimitive.Root>
<MessagePrimitive.Parts />
</MessagePrimitive.Root>Root renders a <div> that provides message context and tracks hover state. Parts iterates over the message's content parts and renders each one. Without custom components, parts render with sensible defaults: Text renders a <p> with white-space: pre-line and a streaming indicator, Image renders via MessagePartPrimitive.Image, and tool calls render nothing unless a tool UI is registered globally or inline. Reasoning, source, file, and audio parts render nothing by default.
Runtime setup: primitives require runtime context. Wrap your UI in AssistantRuntimeProvider with a runtime (for example useLocalRuntime(...)). See Pick a Runtime.
Core Concepts
Parts Pipeline
MessagePrimitive.Parts now prefers a children render function. It gives you the current enriched part state directly, so you can branch inline and return exactly the UI you want:
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MyTextRenderer />;
if (part.type === "image") return <MyImageRenderer />;
if (part.type === "tool-call")
return part.toolUI ?? <GenericToolUI {...part} />;
return null;
}}
</MessagePrimitive.Parts>For most new MessagePrimitive.Parts code, prefer the children render function. Grouped Chain of Thought is the current exception: it plugs into MessagePrimitive.Parts via components.ChainOfThought.
Tool Resolution
Tool call parts resolve in this order:
tools.Override: if provided inline through the deprecatedcomponentsprop, handles all tool calls- Globally registered tools: tools registered via
makeAssistantTool/useAssistantToolUI tools.by_name[toolName]: per-MessagePrimitive.Partsinline overrides from the deprecatedcomponentsproptools.Fallback: catch-all for unmatched tool calls from the deprecatedcomponentsproppart.toolUI: the resolved tool UI exposed directly in the children render function
In the children API, tool and data parts expose resolved UI helpers directly:
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "tool-call")
return part.toolUI ?? <ToolFallback {...part} />;
if (part.type === "data")
return part.dataRendererUI ?? null;
return null;
}}
</MessagePrimitive.Parts>Returning null still allows registered tool UIs and data renderer UIs to render automatically. Return <></> if you want to suppress them entirely.
Components Prop (Deprecated)
components is deprecated. This section only documents it so older code is still understandable:
ToolGroupwraps consecutive tool-call partsReasoningGroupwraps consecutive reasoning partscomponents.ChainOfThoughttakes over all reasoning and tool-call rendering (mutually exclusive withToolGroup,ReasoningGroup,tools, andReasoning). Despite the deprecation ofcomponentsin general, this is still the current way to wire grouped Chain of Thought.data.by_nameanddata.Fallbacklet you route custom data part typesQuoterenders quoted message references from metadataEmptyandUnstable_Audioare available for edge and experimental rendering paths
<MessagePrimitive.Parts
components={{
Text: () => (
<p className="whitespace-pre-wrap">
<MessagePartPrimitive.Text />
</p>
),
Image: () => <MessagePartPrimitive.Image className="max-w-sm rounded-xl" />,
File: () => <div className="rounded-md border px-2 py-1 text-xs">File part</div>,
tools: {
by_name: {
get_weather: () => <div>Weather tool</div>,
},
Fallback: ({ toolName }) => <div>Unknown tool: {toolName}</div>,
},
data: {
by_name: {
"my-event": ({ data }) => <pre>{JSON.stringify(data, null, 2)}</pre>,
},
Fallback: ({ name }) => <div>Unknown data event: {name}</div>,
},
ToolGroup: ({ children }) => (
<div className="space-y-2 rounded-lg border p-2">{children}</div>
),
ReasoningGroup: ({ children }) => (
<details className="rounded-lg border p-2">
<summary>Reasoning</summary>
{children}
</details>
),
Empty: () => <span className="text-muted-foreground">...</span>,
Unstable_Audio: () => null,
}}
/>For new code, use the children render function instead.
Hover State
MessagePrimitive.Root automatically tracks mouse enter/leave events. This hover state is consumed by ActionBarPrimitive to implement auto-hide behavior, with no extra wiring needed.
MessagePartPrimitive
Inside your custom part components, use these sub-primitives to access the actual content:
MessagePartPrimitive.Text: renders the text content of a text partMessagePartPrimitive.Image: renders the image of an image partMessagePartPrimitive.InProgress: renders only while the part is still streaming
function MyText() {
return (
<p className="whitespace-pre-wrap">
<MessagePartPrimitive.Text />
<MessagePartPrimitive.InProgress>
<span className="animate-pulse">โ</span>
</MessagePartPrimitive.InProgress>
</p>
);
}Parts
Root
Container for a single message. Renders a <div> element unless asChild is set.
<MessagePrimitive.Root className="flex flex-col gap-2">
<MessagePrimitive.Quote>
{({ text }) => <blockquote className="mb-2 border-l pl-3 italic">{text}</blockquote>}
</MessagePrimitive.Quote>
<MessagePrimitive.Parts />
</MessagePrimitive.Root>Parts
Renders each content part with type-based component resolution.
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MyTextRenderer />;
if (part.type === "image") return <MyImageRenderer />;
if (part.type === "tool-call")
return part.toolUI ?? <GenericToolUI {...part} />;
return null;
}}
</MessagePrimitive.Parts>Prop
Type
Content
Legacy alias for Parts.
<MessagePrimitive.Content>
{({ part }) => {
if (part.type === "text") return <MyTextRenderer />;
return null;
}}
</MessagePrimitive.Content>PartByIndex
Renders a single part at a specific index.
<MessagePrimitive.PartByIndex
index={0}
components={{ Text: MyTextRenderer }}
/>Attachments
Renders all user message attachments.
<MessagePrimitive.Attachments>
{({ attachment }) => {
if (attachment.type === "image") {
const imageSrc = attachment.content?.find((part) => part.type === "image")?.image;
if (!imageSrc) return null;
return <img src={imageSrc} alt={attachment.name} className="max-w-xs rounded-lg" />;
}
if (attachment.type === "document") {
return (
<div className="rounded-lg border p-2 text-sm">
{attachment.name}
</div>
);
}
return null;
}}
</MessagePrimitive.Attachments>Prop
Type
AttachmentByIndex
Renders a single attachment at the specified index within the current message.
<MessagePrimitive.AttachmentByIndex
index={0}
components={{ Attachment: MyAttachment }}
/>Prop
Type
indexcomponents?Error
Renders children only when the message has an error.
<MessagePrimitive.Error>
<ErrorPrimitive.Root className="mt-2 rounded-md border border-destructive/20 bg-destructive/5 p-3">
<ErrorPrimitive.Message />
</ErrorPrimitive.Root>
</MessagePrimitive.Error>Quote
Renders quote metadata when the current message includes a quote. Place it above MessagePrimitive.Parts.
<MessagePrimitive.Quote>
{({ text, messageId }) => (
<blockquote className="mb-2 border-l pl-3 italic" data-message-id={messageId}>
{text}
</blockquote>
)}
</MessagePrimitive.Quote>Unstable_PartsGrouped
Groups consecutive parts by a custom grouping function (unstable).
<MessagePrimitive.Unstable_PartsGrouped
groupingFunction={myGroupFn}
components={{ Text: MyText, Group: MyGroupWrapper }}
/>Prop
Type
Unstable_PartsGroupedByParentId
Groups parts by parent ID (unstable, deprecated; use Unstable_PartsGrouped).
<MessagePrimitive.Unstable_PartsGroupedByParentId
components={{ Text: MyText, Group: MyGroupWrapper }}
/>If (deprecated)
Deprecated. Use AuiIf instead.
// Before (deprecated)
<MessagePrimitive.If user>...</MessagePrimitive.If>
<MessagePrimitive.If assistant>...</MessagePrimitive.If>
// After
<AuiIf condition={(s) => s.message.role === "user"}>...</AuiIf>
<AuiIf condition={(s) => s.message.role === "assistant"}>...</AuiIf>Patterns
Custom Text Rendering
function MarkdownText() {
return (
<div className="prose prose-sm">
<MessagePartPrimitive.Text />
</div>
);
}
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MarkdownText />;
return null;
}}
</MessagePrimitive.Parts>Tool UI with by_name
<MessagePrimitive.Parts
components={{
Text: MyText,
tools: {
by_name: {
get_weather: ({ result }) => (
<div className="rounded-lg border p-3">
<p className="font-medium">Weather</p>
<p>{result?.temperature}ยฐF, {result?.condition}</p>
</div>
),
},
Fallback: ({ toolName, status }) => (
<div className="text-muted-foreground text-sm">
{status.type === "running" ? `Running ${toolName}...` : `${toolName} completed`}
</div>
),
},
}}
/>Error Display
<MessagePrimitive.Root>
<MessagePrimitive.Parts />
<MessagePrimitive.Error>
<div className="mt-2 rounded-md bg-destructive/10 p-2 text-sm text-destructive">
Something went wrong. Please try again.
</div>
</MessagePrimitive.Error>
</MessagePrimitive.Root>Error Display with ErrorPrimitive
For more control over error rendering, ErrorPrimitive provides a dedicated component that auto-reads the error string from the message status:
import { ErrorPrimitive, MessagePrimitive } from "@assistant-ui/react";
<MessagePrimitive.Root>
<MessagePrimitive.Parts />
<ErrorPrimitive.Root className="mt-2 rounded-md bg-destructive/10 p-2 text-sm text-destructive" role="alert">
<ErrorPrimitive.Message />
</ErrorPrimitive.Root>
</MessagePrimitive.Root>ErrorPrimitive.Root renders a <div> container with role="alert" and ErrorPrimitive.Message renders a <span> that displays the error text. Root always renders. Only Message conditionally returns null when there is no error. Wrap in <MessagePrimitive.Error> if you want the entire block to be conditional. See the ErrorPrimitive API Reference for full details.
Render After Stream Completes
To render content only once the assistant message has finished streaming (a follow-up card, a feedback prompt, a generated component that should not flicker through partial states), gate it with AuiIf on s.message.status:
import { MessagePrimitive, AuiIf } from "@assistant-ui/react";
<MessagePrimitive.Root>
<MessagePrimitive.Parts />
<AuiIf
condition={(s) =>
s.message.role === "assistant" &&
s.message.status?.type === "complete"
}
>
<FollowUpCard />
</AuiIf>
</MessagePrimitive.Root>;s.message.status is a discriminated union of running | requires-action | complete | incomplete, defined only on assistant messages. The role === "assistant" guard keeps the predicate type-safe. For tool-call-driven generative UI that defers rendering inside the part itself, see Deferred Rendering in the Generative UI guide.
Legacy and Unstable APIs
MessagePrimitive.Unstable_PartsGroupedandMessagePrimitive.Unstable_PartsGroupedByParentIdare unstable APIs for custom grouping.Unstable_PartsGroupedByParentIdis deprecated in favor ofUnstable_PartsGrouped.
Role-Based Styling
MessagePrimitive.Root sets data-message-id automatically but does not set a data-role attribute. Style by role in your message components:
// In your ThreadPrimitive.Messages children render function:
function UserMessage() {
return (
<MessagePrimitive.Root data-role="user" className="flex justify-end">
<MessagePrimitive.Parts />
</MessagePrimitive.Root>
);
}
function AssistantMessage() {
return (
<MessagePrimitive.Root data-role="assistant" className="flex justify-start">
<MessagePrimitive.Parts />
</MessagePrimitive.Root>
);
}Attachments
<MessagePrimitive.Root>
<MessagePrimitive.Attachments>
{({ attachment }) => {
if (attachment.type === "image") {
const imageSrc = attachment.content?.find((part) => part.type === "image")?.image;
if (!imageSrc) return null;
return <img src={imageSrc} alt={attachment.name} className="max-w-xs rounded-lg" />;
}
if (attachment.type === "document") {
return (
<div className="flex items-center gap-2 rounded-lg border p-2 text-sm">
๐ {attachment.name}
</div>
);
}
return null;
}}
</MessagePrimitive.Attachments>
<MessagePrimitive.Parts />
</MessagePrimitive.Root>Relationship to Components
The shadcn Thread component renders user and assistant messages built from these primitives. The pre-built AssistantMessage and UserMessage components handle text rendering, tool UIs, error display, and action bars, all using MessagePrimitive under the hood.
Messages are commonly paired with ActionBar for copy/reload/edit actions and BranchPicker for navigating between alternative responses.
API Reference
For full prop details on every part, see the MessagePrimitive API Reference.
Related: