Docs

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" />;
}

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.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>
  );
}

The focused primitives are:

  • Message.Text and Message.Markdown for text parts.
  • Message.Reasoning for reasoning parts.
  • Message.Tool, Message.ToolName, Message.ToolStatus, Message.ToolInput, Message.ToolOutput, and Message.ToolError for tool parts.
  • Message.Attachment for attachment parts.
  • Message.Data for structured data parts.
  • Message.Error for 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>
  );
}

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.

Interactive comparison

Same source events, different display

Without smoothing
Source updates
Waiting
With smoothing
Buffered display
Waiting
Both views receive the same bursty source and finish with identical text. Smoothing changes only the display cadence; the stored message is unchanged.

Use these rules:

  1. Put stream on Message.Parts when a response can contain reasoning, text, and tools. This preserves their reveal order.
  2. Keep the stream prop mounted when the request finishes. Change isStreaming from true to false; removing stream immediately would skip the completion drain.
  3. Use a stable resetKey for the displayed response. message.id is 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.