Skip to content
+

Chat - Headless customization

Customize the headless primitives through slots, slotProps, and owner state while keeping the built-in structure and behavior.

Brand-adapted shell
The runtime and accessibility behavior stay the same. Only the rendered slots change.
Design review
Header spacing and slot strategy
You
You
Should we keep the default thread header or replace the title slot?
8:34 AM · sent
MUI Guide
MUI Guide
Keep the structure. Replace the title and actions slots so the behavior stays intact.
Marco Diaz
Marco Diaz
That also keeps focus restoration predictable when the active thread changes.
8:52 AM · sent

Customization model

@mui/x-chat/headless is designed to let you keep the shipped semantics and interaction logic while replacing most of the rendered structure.

The main tools are:

  • slots to replace structural subcomponents
  • slotProps to pass props into those replacements
  • owner state to style custom slots based on runtime-aware structural state

slots

Use slots when you want to replace the element or React component used for a specific region.

Examples:

  • replace the conversation list root with a custom container
  • replace the conversation row component while preserving listbox behavior
  • replace the composer attach button or hidden file input
  • replace the unread marker label or scroll-to-bottom badge
<ConversationList.Root
  slots={{
    root: 'aside',
    item: CustomConversationRow,
  }}
/>

slotProps

Use slotProps when you want to keep the slot structure but add attributes, styling hooks, or local event behavior.

<Composer.AttachButton
  slotProps={{
    input: {
      accept: 'image/*,.pdf',
    },
    root: {
      'aria-label': 'Upload files',
    },
  }}
/>

slotProps is also the place to pass attributes such as id, className, ARIA labels, and design-system-specific props into the replaced slot.

Owner state

Custom slot components receive owner state that describes the structural state of the primitive.

Common examples include:

  • conversation item state such as selected, unread, and focused
  • thread state such as conversationId and hasConversation
  • message-list state such as isAtBottom and messageCount
  • message state such as role, status, streaming, and isGrouped
  • composer state such as hasValue, isSubmitting, isStreaming, and attachmentCount
  • indicator state such as typing users, unread boundaries, and unseen-message counts

Owner-state example

const CustomSendButton = React.forwardRef(function CustomSendButton(props, ref) {
  const { ownerState, ...other } = props;

  return (
    <button
      data-streaming={String(ownerState?.isStreaming)}
      data-submitting={String(ownerState?.isSubmitting)}
      ref={ref}
      {...other}
    />
  );
});

This pattern is the main bridge between the headless package and a product-specific visual language.

Replace slots or rebuild from core

Use slot replacement when:

  • the shipped behavior is correct
  • the overall structure is close to what you need
  • you want the package to keep handling semantics, focus, and state-derived structural logic

Use headless primitives or custom composition when:

  • the interaction model changes substantially
  • the component hierarchy is fundamentally different
  • the built-in keyboard or list behavior no longer matches the product surface

Styling strategy

The headless docs stay design-system agnostic on purpose. Typical styling approaches include:

  • utility classes
  • CSS Modules
  • CSS-in-JS wrappers
  • custom design-system components passed through slots

The important boundary is:

  • headless owns runtime state and contracts
  • headless owns structure, semantics, and interaction behavior
  • your app owns visual design

See also

  • Continue with Conversation list to see owner state on row-level slots.
  • Continue with Messages for selective message-part replacement.
  • Continue with Slot customization for a full demo that replaces multiple slots in one surface.

API