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 (
) - Headings (
# H1through###### H6) - Ordered and unordered lists
- Code fences (rendered as
ChatCodeBlock) - Footnote citations (
[^1])
Markdown source
Rendered message
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' }
'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
reasoningpart, 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.