What's the status of ticket T-1042?
Chat - Custom parts
Extend the message part system with app-specific content types, custom renderers, and a typed registry.
The built-in part types (text, reasoning, file, source-url, source-document, tool, dynamic-tool, step-start, and data-*) cover common chat patterns.
When the application needs domain-specific content, such as ticket cards, approval forms, charts, or product previews, use the extensibility points described on this page.
Data parts
ChatDataMessagePart is the built-in extensibility point for custom payloads. Data parts use a type string prefixed with data- and carry an arbitrary data payload:
interface ChatDataMessagePart {
type: `data-${string}`;
id?: string;
data: unknown;
transient?: boolean;
}
This is the simplified, un-augmented shape. Once you register an entry in ChatDataPartMap (see Typed data parts), the data field of that part type is narrowed to the registered payload instead of unknown.
Parts with transient: true are delivered during streaming but not persisted in the final message—see Streaming for how transient data chunks arrive over the wire.
The default renderer displays data parts as formatted JSON. Replace it with a custom renderer to control the presentation.
Type registry pattern
Use TypeScript module augmentation to get compile-time safety for custom parts. Two registry interfaces are available for this purpose:
Typed data parts
Add entries to ChatDataPartMap to type the data payload of data-* parts:
declare module '@mui/x-chat/types' {
interface ChatDataPartMap {
'data-ticket-status': {
ticketId: string;
status: 'open' | 'blocked' | 'resolved';
lastUpdated: string;
};
}
}
Once registered, data-ticket-status parts carry typed data instead of unknown.
Adding new part types
Add entries to ChatCustomMessagePartMap to create part types that are not prefixed with data-:
declare module '@mui/x-chat/types' {
interface ChatCustomMessagePartMap {
'ticket-summary': {
type: 'ticket-summary';
summary: string;
ticketId: string;
};
}
}
Custom parts are included in the ChatMessagePart union, so they appear in message.parts and can be rendered through the custom renderer system.
Registering custom renderers
A renderer is a function that receives the part and returns the JSX to display in its place. The following demo registers a data-ticket-status renderer that turns the raw payload into a colored status badge—send a message to see new badges stream in instead of the default JSON fallback.
With ChatProvider
Register renderers on ChatProvider using the partRenderers prop:
import { ChatProvider } from '@mui/x-chat/headless';
// `adapter` is any ChatAdapter — see /x/react-chat/backend/adapters/
<ChatProvider
adapter={adapter}
partRenderers={{
'ticket-summary': ({ part }) => (
<div className="ticket-card">
<h4>Ticket: {part.ticketId}</h4>
<p>{part.summary}</p>
</div>
),
// part.data is typed thanks to the ChatDataPartMap entry above
'data-ticket-status': ({ part }) => (
<span className={`status-badge status-${part.data.status}`}>
{part.data.status}
</span>
),
}}
>
<MyChat />
</ChatProvider>;
When using the Material <ChatBox />, pass the same map through its partRenderers prop—no explicit provider needed.
Looking up renderers
Use useChatPartRenderer() to retrieve a registered renderer in any component:
import { useChatPartRenderer } from '@mui/x-chat/headless';
function MyMessagePart({ part, message }) {
const renderer = useChatPartRenderer(part.type);
if (renderer) {
return renderer({ part, message, index: 0 });
}
return <DefaultFallback part={part} />;
}
Overriding a single part type
When you only need to customize one or two part types and keep defaults for the rest, use getDefaultMessagePartRenderer():
import { getDefaultMessagePartRenderer } from '@mui/x-chat/headless';
function renderPart(part, message, index) {
// Custom rendering for one part type
if (part.type === 'data-ticket-status') {
return <TicketStatusBadge data={part.data} />;
}
// Default rendering for everything else
const renderer = getDefaultMessagePartRenderer(part);
return renderer ? renderer({ part, message, index }) : null;
}
This pattern keeps the override narrow—replace one part type without forking the whole message surface.
How types flow through the stack
Once declared, the augmentation affects everything at compile time:
- Message parts —
message.partsincludes custom parts in its union - Stream chunks — data chunks carry the registered payload types
- Hooks —
useChat().messagesreturns messages with augmented part types - Renderers —
useChatPartRenderer('ticket-summary')returns a typed renderer
No runtime code changes are needed. The augmentation is purely compile-time.
See also
- Text and Markdown for the most common built-in part type.
- Streaming for details on how parts arrive over the wire.
API
See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.