Skip to content
+

Chat - Text and markdown

Render plain text and markdown content in chat messages with the built-in renderer.

Text parts are the most common message part. Every message with written content—whether from a human user or an AI assistant—uses one or more text parts to carry that content.

Text part structure

A text part is represented by the ChatTextMessagePart interface:

interface ChatTextMessagePart {
  type: 'text';
  text: string;
  state?: 'streaming' | 'done';
}

A message can contain multiple text parts alongside other part types (files, sources, tool calls). The parts array on ChatMessage holds them all:

const message: ChatMessage = {
  id: 'msg-1',
  role: 'assistant',
  parts: [
    { type: 'text', text: 'Here is the analysis you requested:' },
    {
      type: 'file',
      mediaType: 'image/png',
      url: '/chart.png',
      filename: 'chart.png',
    },
    { type: 'text', text: 'The chart shows a 15% increase in Q4.' },
  ],
};

The reasoning part shares the same structure (text plus optional state) and is rendered with the same markdown renderer — see Reasoning.

Markdown rendering

When using ChatBox with the Material UI layer (@mui/x-chat), text parts are rendered through a built-in markdown parser that converts common markdown syntax into React elements. This happens automatically, with no configuration needed.

The default renderer is streaming-aware: it repairs half-streamed markdown as tokens arrive, so partial syntax renders cleanly instead of leaking raw ** or unclosed code-fence markers mid-stream.

The built-in parser supports:

  • Bold (**text** or __text__)
  • Italic (*text* or _text_)
  • Inline code (`code`)
  • Links ([label](url))
  • Images (![alt](url))
  • Headings (# H1 through ###### H6)
  • Ordered and unordered lists
  • Code fences (rendered as ChatCodeBlock)
  • Footnote citations ([^1])
Markdown source
Rendered message
Assistant

Markdown rendering

The built-in renderer turns model output into bold, italic, and inline code.

  • First item
  • Second item with a link
  • Third item
ts
const greeting: string = 'Hello, markdown!';
console.log(greeting);

Footnotes are supported too.1

<b>Raw HTML stays escaped text.</b>

1: This is the footnote definition.

Customizing text rendering

Override the markdown renderer through partProps.text.renderText on ChatMessageContent:

<ChatBox
  adapter={adapter}
  slotProps={{
    content: {
      partProps: {
        text: {
          renderText: (text) => <MyCustomMarkdownRenderer content={text} />,
        },
      },
    },
  }}
/>

renderText receives the part's raw text string and returns any React node — the example below in Streaming text shows what you give up when replacing the built-in renderer.

This lets you plug in any markdown library (react-markdown, remark, MDX) while keeping the rest of the chat UI intact. Replacing renderText replaces the entire pipeline for that part — including streaming repair and code-fence routing. Code fences are rendered by the same built-in renderer, so a custom renderText also takes over code-block rendering — see Code blocks for rendering fences with your own component.

Streaming text

Text parts support incremental delivery through the streaming protocol. The stream uses three chunk types to build up a text part:

Chunk type Purpose
text-start Opens a new text part
text-delta Appends a string fragment
text-end Marks the text part as complete

While tokens are arriving, the part's state field is 'streaming'. Once the text-end chunk arrives, state transitions to 'done', as shown below:

// During streaming
{ type: 'text', text: 'The answer is', state: 'streaming' }

// After completion
{ type: 'text', text: 'The answer is 42.', state: 'done' }
Streaming markdown

Assistant

Send anything — I reply with streaming markdown.

'use client';
import * as React from 'react';
import { ChatBox } from '@mui/x-chat';
import type {
  ChatAdapter,
  ChatConversation,
  ChatMessage,
} from '@mui/x-chat/headless';
import {
  createChunkStream,
  createTextResponseChunks,
  randomId,
} from 'docs/data/chat/core/examples/shared/demoUtils';

// A markdown-rich answer whose bold span and fenced code block are deliberately
// long enough that mid-stream chunks split inside `**…**` and inside the fence —
// so you can watch the built-in streaming repair render them cleanly instead of
// flashing raw `**` or an unclosed code fence.
const MARKDOWN_REPLY = `**Here is what I found** for your request:

1. Markdown streams in token by token.
2. Partial **bold** and code fences are repaired mid-stream.
3. The bubble never leaks raw \`**\` markers.

\`\`\`ts
function greet(name: string): string {
  return \`Hello, \${name}!\`;
}
\`\`\`
`;

const conversation: ChatConversation = {
  id: 'streaming-markdown',
  title: 'Streaming markdown',
  participants: [],
};

const messages: ChatMessage[] = [
  {
    id: 'streaming-markdown-welcome',
    conversationId: conversation.id,
    role: 'assistant',
    status: 'sent',
    createdAt: '2026-03-15T10:00:00.000Z',
    parts: [
      { type: 'text', text: 'Send anything — I reply with streaming markdown.' },
    ],
  },
];

const streamingAdapter: ChatAdapter = {
  async sendMessage() {
    return createChunkStream(createTextResponseChunks(randomId(), MARKDOWN_REPLY), {
      delayMs: 60,
    });
  },
};

export default function StreamingMarkdown() {
  return (
    <ChatBox
      adapter={streamingAdapter}
      initialActiveConversationId={conversation.id}
      initialConversations={[conversation]}
      initialMessages={messages}
      sx={{
        height: 460,
        border: '1px solid',
        borderColor: 'divider',
        borderRadius: 1,
      }}
    />
  );
}

When composing a fully custom message renderer (for example with the headless package), use the part's state field to show a typing indicator or pulsing cursor while content is arriving:

function TextPartDisplay({ part }: { part: ChatTextMessagePart }) {
  return (
    <span>
      {part.text}
      {part.state === 'streaming' && <span className="cursor" />}
    </span>
  );
}

The built-in Material UI renderer already handles this: it stays streaming-aware and repairs partial markdown automatically (see Markdown rendering). Note that partProps.text.renderText receives only the raw text string — if you need the part's state, you must take over part rendering entirely.

See also

  • See Scrolling for how the message list follows streaming content.
  • See Reasoning for the reasoning part, which shares the text part's structure and renderer.
  • See Code blocks for details on syntax-highlighted code fence rendering.
  • See Message appearance for details on the visual presentation of the message list.

API

See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.