Skip to content
+

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.

User

What's the status of ticket T-1042?

Assistant

Here is the latest status:

T-1042 · blockedupdated 2026-06-10

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:

  1. Message partsmessage.parts includes custom parts in its union
  2. Stream chunks — data chunks carry the registered payload types
  3. HooksuseChat().messages returns messages with augmented part types
  4. RenderersuseChatPartRenderer('ticket-summary') returns a typed renderer

No runtime code changes are needed. The augmentation is purely compile-time.

See also

API

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