Skip to content
+

Chat - Conversation List

Render a sidebar that lists all conversations and lets users switch between them.

ChatConversationList is the inbox sidebar for multi-conversation apps — reach for it when users manage several threads; a single-thread assistant can use ChatBox without it. It's a single component with styled slots for every visual sub-region: the scroller, each item row, the avatar, the title, the preview line, the timestamp, the unread badge, and the per-row actions button.

Interactive playground

Toggle the list variant, conversation count, and unread badges:

ChatConversationList
Inbox sidebar — avatar, title, preview, timestamp and unread badge.

MUI Assistant
Styling questions
Theming MuiChatComposer
3
MUI Assistant
RAG with sources
Citations + retrieval
MUI Assistant
Code review
Discussing the codeblock slot
Planning
Sprint scope & owners
1
props
variantenum · 2
fixture data
conversation count4
active index0
Maps to ScopedChat.activeConversationId.
mark every conversation unread

The example below shows a complete two-pane inbox using controlled state:

Component questions

Which component handles the composer?

Alice Chen
Alice Chen

Which component should I use for the message input area?

MUI Assistant
MUI Assistant

The composer is handled by the ChatBox automatically. You can override it with slots.composer.

Alice Chen
Alice Chen

And what about slotProps for passing sx to the input?

Component anatomy

ChatConversationList renders one row per conversation. The default row structure is:

ChatConversationList              <- scrolling listbox (role="listbox")
  [per conversation]
  item                            <- row container (role="option")
    itemAvatar                    <- 40x40 circular avatar
    itemContent                   <- flex column: title + preview
      title                       <- conversation name (bold when unread)
      preview                     <- last message text (caption, truncated)
    timestamp                     <- last-message time (caption, right-aligned)
    unreadBadge                   <- count badge (primary.main background)

All visual slots are owned by a single ChatConversationList instance. You do not need to compose subcomponents manually—instead you replace any slot through the slots prop directly on ChatConversationList.

Variants

The variant prop switches the row layout. It accepts 'default' (the default) and 'compact':

  • 'default' renders the full row: avatar, title, preview line, timestamp, and unread badge.
  • 'compact' renders a denser row: the unreadBadge collapses to an 8px dot, itemAvatar, preview, and timestamp are not rendered, and the itemActions button appears on hover or keyboard focus.

The compact row structure is:

ChatConversationList              <- scrolling listbox (role="listbox")
  [per conversation]
  item                            <- row container (role="option")
    unreadBadge                   <- collapses to an 8px dot
    itemContent                   <- flex column
      title                       <- conversation name (bold when unread)
    itemActions                   <- 3-dot button, revealed on hover/focus

When variant="compact", the root carries the .MuiChatConversationList-compact class hook, so you can target the compact layout from a theme override or sx.

default
MUI Assistant
Styling questions
Theming MuiChatComposer
3
MUI Assistant
RAG with sources
Citations + retrieval
MUI Assistant
Code review
Discussing the codeblock slot
Planning
Sprint scope & owners
1
compact
3
Styling questions
RAG with sources
Code review
1
Planning

Slot reference

Slot key Default component CSS class Purpose
root styled div .MuiChatConversationList-root The outer list container
scroller styled div .MuiChatConversationList-scroller Scrolling column with border-right; its width is set by the ChatBox layout
viewport styled div Scrollable overflow container
scrollbar no-op div Custom scrollbar track (replaced by native overflow)
scrollbarThumb no-op div Custom scrollbar thumb
item styled div .MuiChatConversationList-item Individual row, receives selection/focus styles
itemAvatar ConversationListItemAvatar wrapper .MuiChatConversationList-itemAvatar Circular avatar element
itemContent ConversationListItemContent wrapper .MuiChatConversationList-itemContent Title + preview column
title ConversationListTitle wrapper .MuiChatConversationList-itemTitle Conversation name text
preview ConversationListPreview wrapper .MuiChatConversationList-itemPreview Preview line text
timestamp ConversationListTimestamp wrapper .MuiChatConversationList-itemTimestamp Timestamp text
unreadBadge ConversationListUnreadBadge wrapper .MuiChatConversationList-itemUnreadBadge Unread count pill
itemActions ConversationListItemActions wrapper .MuiChatConversationList-itemActions Per-row actions affordance (3-dot button); rendered only in the compact variant, revealed on hover/focus

Sub-slot class suffixes carry an item prefix that the slot keys drop — slots.title styles via .MuiChatConversationList-itemTitle.

The itemActions slot only renders when variant="compact"; replace it to attach per-row menus (rename, delete, and so on). See Variants for the compact row layout.

Because ChatConversationList provides a default for every slot, overriding a single slot only affects that region without disturbing the others.

For the exhaustive prop and slot listing, see the ChatConversationList API reference.

Switching conversations

The conversation list is rendered when ChatBox is given features={{ conversationList: true }}. Clicking a row calls onActiveConversationChange with the conversation ID. The callback receives string | undefined — it fires with undefined when the active conversation is cleared (for example, when the user navigates back from the thread pane on narrow layouts, or when the active conversation is removed by a real-time event), so guard before assigning it to non-nullable state. Use controlled state to manage the active conversation:

const [activeConversationId, setActiveConversationId] = React.useState('thread-a');

<ChatBox
  activeConversationId={activeConversationId}
  onActiveConversationChange={(nextId) => {
    if (nextId) {
      setActiveConversationId(nextId);
    }
  }}
  conversations={conversations}
  features={{ conversationList: true }}
/>;

If you omit that feature flag, ChatBox renders the thread pane directly without a sidebar.

Reading row state through ownerState

The item and all its sub-slots receive an ownerState prop that carries the current row's interaction state alongside the full conversation object.

Item ownerState

Field Type Description
selected boolean This conversation is the active thread
unread boolean The conversation has unread messages
focused boolean This row currently has keyboard focus
conversation ChatConversation The full conversation data object
variant 'default' | 'compact' | undefined The list variant the row is rendered in

The selected flag drives the row background (palette.action.selected). The unread flag drives bold title typography. A row counts as unread when conversation.unreadCount is greater than 0 or conversation.readState is 'unread' — set either field on the conversation object. The focused flag drives the focus-visible outline for keyboard accessibility.

Because the full conversation object is included, custom slot components can directly read fields such as conversation.title, conversation.metadata, conversation.unreadCount, and conversation.lastMessageAt without additional selectors. Custom item slots can branch on ownerState.variant to render a different layout per variant (see Variants).

Root ownerState

Field Type Description
conversationCount number Total number of conversations
activeConversationId string | undefined Currently selected conversation ID
variant 'default' | 'compact' The list variant currently applied

Overriding the avatar

Replace the avatar slot with a custom component to render initials or a status ring:

import { ChatConversationList } from '@mui/x-chat';
import Avatar from '@mui/material/Avatar';

const ThemedAvatar = React.forwardRef(function ThemedAvatar(
  { ownerState, ...props },
  ref,
) {
  const title = ownerState?.conversation?.title ?? '';
  const initials = title
    .split(' ')
    .slice(0, 2)
    .map((word) => word[0])
    .join('')
    .toUpperCase();

  return (
    <Avatar
      ref={ref}
      {...props}
      sx={{
        width: 40,
        height: 40,
        bgcolor: ownerState?.selected ? 'primary.main' : 'grey.300',
        fontSize: 'body2.fontSize',
        fontWeight: 'fontWeightMedium',
        ...props.sx,
      }}
    >
      {initials}
    </Avatar>
  );
});

<ChatConversationList slots={{ itemAvatar: ThemedAvatar }} />;

Every slot component receives ownerState as a prop. Destructure it before spreading ...props to avoid forwarding a non-standard attribute to the DOM.

Overriding the item content layout

Replace itemContent when you want to change the structural layout of the title and preview region—for example, to add a participant count or an icon:

import { ChatConversationList } from '@mui/x-chat';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import GroupIcon from '@mui/icons-material/Group';

const RichItemContent = React.forwardRef(function RichItemContent(
  { ownerState, children, ...props },
  ref,
) {
  const { conversation, unread } = ownerState ?? {};

  return (
    <Box
      ref={ref}
      {...props}
      sx={{ display: 'flex', flexDirection: 'column', minWidth: 0, flex: 1 }}
    >
      <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
        <Typography
          variant="body2"
          noWrap
          sx={{
            fontWeight: unread ? 'fontWeightBold' : 'fontWeightMedium',
            flex: 1,
          }}
        >
          {conversation?.title}
        </Typography>
        {(conversation?.participants?.length ?? 0) > 2 && (
          <GroupIcon sx={{ fontSize: 14, color: 'text.disabled' }} />
        )}
      </Box>
      <Typography variant="caption" color="text.secondary" noWrap>
        {conversation?.subtitle ?? 'No messages yet'}
      </Typography>
    </Box>
  );
});

<ChatConversationList slots={{ itemContent: RichItemContent }} />;

When you replace itemContent, the title and preview slots are no longer rendered (they are children of the default itemContent). Render any equivalent content directly inside your custom component.

Overriding the full item row

Replace the item slot to take full control of a row's layout while still benefiting from the built-in selection, keyboard navigation, and aria-selected wiring:

import { ChatConversationList } from '@mui/x-chat';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';

const CompactRow = React.forwardRef(function CompactRow(
  { ownerState, ...props },
  ref,
) {
  const { conversation, selected, unread } = ownerState ?? {};

  return (
    <Box
      ref={ref}
      {...props}
      sx={{
        display: 'flex',
        alignItems: 'center',
        gap: 1,
        px: 1.5,
        py: 0.75,
        cursor: 'pointer',
        bgcolor: selected ? 'action.selected' : 'transparent',
        '&:hover': { bgcolor: selected ? 'action.selected' : 'action.hover' },
        '&:focus-visible': {
          outline: '2px solid',
          outlineColor: 'primary.main',
          outlineOffset: -2,
        },
        borderRadius: 1,
        mx: 0.5,
      }}
    >
      <Box
        sx={{
          width: 8,
          height: 8,
          borderRadius: '50%',
          bgcolor: unread ? 'primary.main' : 'transparent',
          flexShrink: 0,
        }}
      />
      <Typography
        variant="body2"
        noWrap
        sx={{ fontWeight: unread ? 'fontWeightBold' : 'fontWeightRegular', flex: 1 }}
      >
        {conversation?.title ?? 'Untitled'}
      </Typography>
    </Box>
  );
});

<ChatConversationList slots={{ item: CompactRow }} />;

The role="option" and aria-selected attributes are set automatically before the slot renders, so they are present on the element even without the default styled item. Spread ...props to pass them through.

Full custom item renderer

The conversation object in ownerState lets you derive everything you need to render a rich row without additional data fetching or selectors. The demo below builds a full item renderer that shows an avatar with initials, a bold title for unread conversations, a truncated preview, a human-readable timestamp, and a count badge:

SQ

Styling questions

45d
Theming MuiChatComposer
RW

RAG with sources

46d
Citations + retrieval
CR

Code review

47d
Discussing the codeblock slot
P

Planning

47d
Sprint scope & owners

A small helper turns the ISO lastMessageAt field into a relative label:

function formatRelativeTime(iso?: string) {
  if (!iso) return '';
  const diff = Date.now() - new Date(iso).getTime();
  if (diff <= 0) return 'now';
  const minutes = Math.floor(diff / 60_000);
  if (minutes < 1) return 'now';
  if (minutes < 60) return `${minutes}m`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h`;
  return `${Math.floor(hours / 24)}d`;
}

Inside the row component, destructure ownerState and spread ...props so the built-in selection, role="option", aria-selected, and keyboard handlers survive:

const FullCustomRow = React.forwardRef(function FullCustomRow(
  { ownerState, ...props },
  ref,
) {
  const { conversation, selected, unread } = ownerState ?? {};
  // ...derive initials, render Badge + Avatar + title + timestamp...
  return (
    <Box
      ref={ref}
      {...props}
      sx={
        {
          /* row layout */
        }
      }
    />
  );
});

Pass it to ChatConversationList through the item slot — or to ChatBox via slotProps.conversationList:

<ChatConversationList slots={{ item: FullCustomRow }} />;

<ChatBox
  slotProps={{
    conversationList: {
      slots: { item: FullCustomRow },
    },
  }}
/>;

Styling without slot replacement

If the default row structure suits your product but you only need to change colors or typography, use theme component overrides instead of replacing slots:

import type {} from '@mui/x-chat/themeAugmentation';
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  components: {
    MuiChatConversationList: {
      styleOverrides: {
        item: ({ ownerState }) => ({
          borderRadius: 8,
          marginInline: 4,
          ...(ownerState?.selected && {
            backgroundColor: 'var(--mui-palette-primary-main)',
            color: 'var(--mui-palette-primary-contrastText)',
          }),
        }),
        title: ({ ownerState }) => ({
          fontWeight: ownerState?.unread ? 700 : 400,
        }),
      },
    },
  },
});

Theme overrides apply globally across the application and are the lowest-friction option when only visual adjustments are needed.

Controlling the list width

The conversation list width is driven by the scroller slot. Override it through slotProps:

<ChatConversationList
  slotProps={{
    scroller: { sx: { width: 320 } },
  }}
/>

Or set the --ChatBox-conversationListWidth CSS variable on ChatBox to control the width from a layout level:

<ChatBox
  features={{ conversationList: true }}
  sx={{ '--ChatBox-conversationListWidth': '320px' }}
/>

The CSS variable is read by ChatBox's layout pane — when rendering ChatConversationList standalone, size it through the scroller slot or its parent container instead.

Accessibility notes

The default list uses role="listbox" on the root and role="option" with aria-selected on each row. The component manages roving focus: only one row is in the tab order at a time, and ArrowUp, ArrowDown, Home, End, and Enter are handled automatically.

Custom item slot components must forward all ...props to the DOM element they render so the role, aria-selected, and keyboard handler props are preserved. Failing to spread ...props breaks both keyboard navigation and screen-reader semantics.

Pass aria-label to the root through slotProps:

<ChatConversationList slotProps={{ root: { 'aria-label': 'Conversations' } }} />

The message list shares the same roving-focus model, so both lists feel identical to keyboard users.

See also

API

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