React UI
Messages
Start with the default message renderer, then customize text, Markdown, tool, attachment, data, and error parts.
Message.Root renders one Anvia UIMessage. Message.Parts renders the message’s streamed
parts. Start with the default renderer, then replace only the parts that need product-specific UI.
One message can contain many parts:
const message = {
id: "assistant-1",
role: "assistant",
parts: [
{ id: "text-1", type: "text", text: "Order A-100 is blocked." },
{
id: "tool-1",
type: "tool",
toolName: "lookup_order",
toolCallId: "call_1",
state: "output-available",
input: { orderId: "A-100" },
output: { status: "blocked" },
},
],
};
Renderer levels
| Level | Use it when | What you write |
|---|---|---|
| Default renderer | You want the package to render every supported part. | <Thread.Messages /> or <Message.Parts /> |
| Styled default renderer | You like the default part behavior but need app layout and actions. | Message.Root, Message.Content, Message.Parts, Message.Actions |
| Custom renderer | Text, tools, attachments, or errors need product-specific markup. | Message.Parts with a child function |
Default renderer
Thread.Messages can render a complete list without a child function. Internally, each row uses
Message.Root, Message.Content, and Message.Parts. The emitted attributes let your app style
user and assistant messages without taking over rendering.
import { Thread } from "@anvia/react-ui";
export function MessageList() {
return <Thread.Messages className="messages" />;
}.messages {
width: min(760px, 100%);
margin: 0 auto;
display: grid;
gap: 16px;
}
.messages [data-anvia-message] {
display: grid;
gap: 8px;
}
.messages [data-role="user"] {
justify-items: end;
}
.messages [data-role="assistant"] {
justify-items: start;
}import { Thread } from "@anvia/react-ui";
export function MessageList() {
return (
<Thread.Messages
className="
mx-auto grid w-full max-w-3xl gap-4
[&_[data-anvia-message]]:grid [&_[data-anvia-message]]:gap-2
[&_[data-role=assistant]]:justify-items-start
[&_[data-role=user]]:justify-items-end
"
/>
);
}This is the renderer version to reach for first. It supports text, reasoning, tool calls,
attachments, data, and errors through Message.Part.
Styled message rows
Add a child function to Thread.Messages when the row needs app-owned layout, copy/regenerate
actions, or design-system wrappers. The part renderer can still stay default.
import { Message, Thread } from "@anvia/react-ui";
export function MessageList() {
return (
<Thread.Messages className="messages">
{(message) => (
<Message.Root className="message">
<Message.Content className="message-content">
<Message.Parts />
</Message.Content>
{message.role === "assistant" ? (
<Message.Actions className="message-actions">
<Message.Copy>Copy</Message.Copy>
<Message.Regenerate>Regenerate</Message.Regenerate>
</Message.Actions>
) : null}
</Message.Root>
)}
</Thread.Messages>
);
}.message {
display: grid;
gap: 8px;
}
.message[data-role="user"] {
justify-items: end;
}
.message-content {
max-width: min(680px, 100%);
}
.message[data-role="user"] .message-content {
max-width: min(620px, 88%);
border-radius: 18px;
background: #27272a;
padding: 10px 14px;
}
.message[data-role="assistant"] .message-content {
line-height: 1.7;
}
.message-actions {
display: flex;
gap: 6px;
color: #a1a1aa;
}import { Message, Thread } from "@anvia/react-ui";
export function MessageList() {
return (
<Thread.Messages className="mx-auto grid w-full max-w-3xl gap-4">
{(message) => (
<Message.Root className="group/message grid gap-2 data-[role=user]:justify-items-end">
<Message.Content
className="
max-w-[min(680px,100%)] leading-7
group-data-[role=user]/message:max-w-[min(620px,88%)]
group-data-[role=user]/message:rounded-[18px]
group-data-[role=user]/message:bg-zinc-800
group-data-[role=user]/message:px-3.5
group-data-[role=user]/message:py-2.5
"
>
<Message.Parts />
</Message.Content>
{message.role === "assistant" ? (
<Message.Actions className="flex gap-1.5 text-zinc-400">
<Message.Copy>Copy</Message.Copy>
<Message.Regenerate>Regenerate</Message.Regenerate>
</Message.Actions>
) : null}
</Message.Root>
)}
</Thread.Messages>
);
}Message.Root emits data-role, so one row component can style user, assistant, system, and
tool messages differently.
Custom part rendering
Use a Message.Parts child function when one part type needs a richer component. Keep
<Message.Part /> as the fallback so new part types still render.
import { Message } from "@anvia/react-ui";
export function MessageBody() {
return (
<Message.Content className="message-content">
<Message.Parts>
{(part) => {
if (part.type === "text") {
return (
<Message.Part className="text-part">
<Message.Markdown />
</Message.Part>
);
}
if (part.type === "attachment") {
return (
<Message.Part className="attachment-part">
<Message.Attachment className="attachment-card" />
</Message.Part>
);
}
if (part.type === "tool") {
return (
<Message.Part className="tool-part">
<Message.Tool className="tool-card" renderWhen="always" />
</Message.Part>
);
}
return <Message.Part className="message-part" />;
}}
</Message.Parts>
</Message.Content>
);
}.text-part {
line-height: 1.7;
}
.attachment-card,
.tool-card {
width: min(560px, 100%);
border: 1px solid #3f3f46;
border-radius: 10px;
background: #18181b;
padding: 12px;
}
.tool-card {
display: grid;
gap: 8px;
}
.message-part[data-part="error"] {
border-left: 3px solid #fb7185;
padding-left: 12px;
color: #fecdd3;
}import { Message } from "@anvia/react-ui";
export function MessageBody() {
return (
<Message.Content className="leading-7">
<Message.Parts>
{(part) => {
if (part.type === "text") {
return (
<Message.Part className="max-w-none leading-7">
<Message.Markdown />
</Message.Part>
);
}
if (part.type === "attachment") {
return (
<Message.Part>
<Message.Attachment className="w-full max-w-xl rounded-lg border border-zinc-700 bg-zinc-900 p-3" />
</Message.Part>
);
}
if (part.type === "tool") {
return (
<Message.Part>
<Message.Tool
className="grid w-full max-w-xl gap-2 rounded-lg border border-zinc-700 bg-zinc-900 p-3"
renderWhen="always"
/>
</Message.Part>
);
}
return <Message.Part className="data-[part=error]:border-l-2 data-[part=error]:border-rose-400 data-[part=error]:pl-3" />;
}}
</Message.Parts>
</Message.Content>
);
}The focused primitives are:
Message.TextandMessage.Markdownfor text parts.Message.Reasoningfor reasoning parts.Message.Tool,Message.ToolName,Message.ToolStatus,Message.ToolInput,Message.ToolOutput, andMessage.ToolErrorfor tool parts.Message.Attachmentfor attachment parts.Message.Datafor structured data parts.Message.Errorfor error parts.
Use the fallback renderer for part types that your app does not explicitly customize:
<Message.Parts>
{(part) => {
if (part.type === "text") {
return (
<Message.Part>
<Message.Markdown />
</Message.Part>
);
}
return <Message.Part />;
}}
</Message.Parts>
Markdown text
Use Message.Markdown when assistant text should render GitHub-flavored Markdown. It accepts
components for links, headings, code blocks, and other Markdown elements.
import { Message } from "@anvia/react-ui";
export function MarkdownTextPart() {
return (
<Message.Part className="text-part">
<Message.Markdown
components={{
code(props) {
return <Message.CodeBlock {...props} />;
},
a(props) {
return <a {...props} target="_blank" rel="noreferrer" />;
},
}}
/>
</Message.Part>
);
}.text-part [data-anvia-markdown] {
display: grid;
gap: 12px;
}
.text-part [data-anvia-code-block] {
max-width: 100%;
overflow-x: auto;
border-radius: 10px;
background: #09090b;
padding: 12px;
}
.text-part [data-anvia-inline-code] {
border-radius: 4px;
background: #27272a;
padding: 1px 4px;
}import { Message } from "@anvia/react-ui";
export function MarkdownTextPart() {
return (
<Message.Part
className="
[&_[data-anvia-markdown]]:grid [&_[data-anvia-markdown]]:gap-3
[&_[data-anvia-code-block]]:max-w-full
[&_[data-anvia-code-block]]:overflow-x-auto
[&_[data-anvia-code-block]]:rounded-lg
[&_[data-anvia-code-block]]:bg-zinc-950
[&_[data-anvia-code-block]]:p-3
[&_[data-anvia-inline-code]]:rounded
[&_[data-anvia-inline-code]]:bg-zinc-800
[&_[data-anvia-inline-code]]:px-1
"
>
<Message.Markdown />
</Message.Part>
);
}Composer entities
When a message contains valid metadata.composer.entities, Message.Markdown renders each entity
in an eligible prose position with headless semantic markup:
<span
data-anvia-message-entity
data-entity-id="document-1"
data-trigger-id="documents"
>
@Guide.pdf
</span>
Style the attributes from application CSS without rewriting mentions into Markdown links:
[data-anvia-message-entity][data-trigger-id="documents"] {
color: var(--document-mention-color);
font-weight: 600;
}
Use renderEntity to replace the complete output, or render Message.Entity directly:
<Message.Markdown
renderEntity={(entity) => <DocumentMention entity={entity} />}
/>
<Message.Entity entity={entity} className="document-mention" />
Ranges must contain integer UTF-16 offsets, stay inside the final message text, match
entity.text, and not overlap. Invalid, stale, overlapping, or out-of-bounds entries fall back to
ordinary text. Entities render in prose, including emphasis and link labels; ranges inside inline
code, fenced code, or link destinations remain literal. Regular Markdown links, consumer remark
plugins, and components.span overrides continue to work. Entity data is never serialized into
the DOM.
Opt-in stream smoothing
Stream smoothing changes only what is displayed. useChat(...) still receives and stores the
complete source message immediately.
Same source events, different display
Source updates
Buffered display
Use these rules:
- Put
streamonMessage.Partswhen a response can contain reasoning, text, and tools. This preserves their reveal order. - Keep the
streamprop mounted when the request finishes. ChangeisStreamingfromtruetofalse; removingstreamimmediately would skip the completion drain. - Use a stable
resetKeyfor the displayed response.message.idis the normal choice and also prevents loaded history from replaying.
The recommended mixed-part integration is:
<Thread.Messages>
{(message) => {
const isAssistant = message.role === "assistant";
const isLatestAssistant = isAssistant && chat.messages.at(-1)?.id === message.id;
const stream = {
// true while source events arrive; false starts the final drain
isStreaming: isLatestAssistant && chat.status === "streaming",
// keep stable for this response; change it when switching responses
resetKey: message.id,
// errors should show their complete source text immediately
flushImmediately: isLatestAssistant && chat.status === "error",
};
return (
<Message.Root>
<Message.Content>
<Message.Parts {...(isAssistant ? { stream } : {})}>
{(part) => (part.type === "text" ? <Message.Markdown /> : <Message.Part />)}
</Message.Parts>
</Message.Content>
</Message.Root>
);
}}
</Thread.Messages>
Message.Parts smooths text and reasoning parts. A new tool part waits until preceding buffered
text is visible, while updates to a tool that is already visible render immediately.
Stream lifecycle
| Option | Set it to | What happens |
|---|---|---|
isStreaming |
true only while source events are arriving |
The display follows the buffered source timeline. |
isStreaming |
false after completion or stop |
Remaining buffered text drains smoothly, usually within 700 ms. |
resetKey |
A stable message or session identifier | Existing content seeds immediately when the identifier changes. |
flushImmediately |
true for a terminal error or forced completion |
The display synchronizes to the complete source without draining. |
Do not conditionally remove stream as soon as chat.status becomes "idle". Keep it present for
assistant messages and let isStreaming: false finish the drain:
// Avoid: this removes smoothing before its buffered tail can drain.
{chat.status === "streaming" ? (
<Message.Parts stream={{ isStreaming: true, resetKey: message.id }} />
) : (
<Message.Parts />
)}
// Use: the same lifecycle remains mounted through completion.
<Message.Parts
stream={{
isStreaming: chat.status === "streaming",
resetKey: message.id,
}}
/>
Text-only rendering
If the surface renders one text value and has no later tool parts to order, put the same lifecycle
directly on Message.Text or Message.Markdown:
<Message.Markdown
stream={{
isStreaming: isLatestAssistant && chat.status === "streaming",
resetKey: message.id,
flushImmediately: isLatestAssistant && chat.status === "error",
}}
/>
This smooths only Markdown text. Use Message.Parts instead when tool cards must wait behind text.
The standalone StreamMarkdown component does not perform pacing by itself; it renders whatever
display string its content prop receives.
The fixed pacing adapts to source throughput, preserves grapheme boundaries, bounds its backlog,
and drains completion without a snap. The Markdown renderer keeps completed blocks stable and
applies the live-tail opacity treatment only to the growing final block. Pacing works without the
optional stylesheet; import @anvia/react-ui/styles.css to enable the 180 ms opacity settle.
Filtering parts
Message.Parts accepts filter when a surface should hide reasoning, show only pending tools, or
split parts between two panels.
<Message.Parts filter={(part) => part.type !== "reasoning"}>
{(part) => (part.type === "tool" ? <ToolActivity part={part} /> : <Message.Part />)}
</Message.Parts>
Use filtering for layout decisions, not data mutation. The source message stays unchanged.
Actions
Message.Copy copies text from the current message. Message.Regenerate calls the chat
controller’s regenerate() action and enables only for the latest assistant message while the chat
is idle.
<Message.Actions className="message-actions">
<Message.Copy asChild>
<button type="button">Copy answer</button>
</Message.Copy>
<Message.Regenerate asChild>
<button type="button">Try again</button>
</Message.Regenerate>
</Message.Actions>
For a larger message recipe, see Message rendering. For server-side tools that produce tool parts, see Tools end to end.
