Skip to content
+

Chat - Loading and empty states

Display loading skeletons while messages load and empty state content when a conversation has no messages.

Displaying a loading skeleton

ChatMessageSkeleton renders animated shimmer lines that serve as a placeholder while message content is loading. Use it during initial data fetching or when loading older messages via history pagination.

Interactive playground

Use the demo below to adjust the number of shimmer lines:

ChatMessageSkeleton
Animated placeholder while a message is loading.

props
lines3

Importing the component

import { ChatMessageSkeleton } from '@mui/x-chat';

Rendering a skeleton

<ChatMessageSkeleton />

By default, the skeleton renders three shimmer lines. The last line is shorter (60% width) to mimic the natural shape of a message.

Configuring the number of lines

Set the lines prop to control how many shimmer lines are displayed:

<ChatMessageSkeleton lines={2} />
<ChatMessageSkeleton lines={5} />

Using the skeleton while messages load

Nothing renders the skeleton automatically — you decide where it appears. Render skeletons in the message area while the fetch is in flight, then swap to ChatMessageList once the data arrives. Set aria-busy on the swapping container and aria-hidden on each skeleton so assistive technology treats the placeholders as decorative.

When older messages are being fetched via history pagination, render a skeleton at the top of the list instead. The isLoadingHistory flag from useChat() reports when a page is in flight. It is true during the initial history fetch and during loadMoreHistory, which is why the same flag drives both placements:

const { isLoadingHistory } = useChat();
// At the top of the message area:
{
  isLoadingHistory && <ChatMessageSkeleton lines={2} aria-hidden />;
}

See History and pagination for the full pagination API.

Slots

Slot Default element Description
root div The outer container
line div Each animated shimmer line

Customize the skeleton appearance through slot replacement:

<ChatMessageSkeleton
  slots={{
    root: MySkeletonRoot,
    line: MySkeletonLine,
  }}
/>

CSS classes

Class name Description
.MuiChatMessageSkeleton-root Root container
.MuiChatMessageSkeleton-line Individual line

Accessibility

ChatMessageSkeleton is purely decorative: it renders plain div elements with no role or aria-* attributes of its own, so assistive technology is not informed that messages are loading.

The Chat components announce other lifecycle stages automatically—the message list is a role="log" polite live region for arriving messages, a hidden role="status" region announces streaming transitions, and a streaming message carries aria-busy="true"—but none of these fire during initial or history loading. Wire the loading state up explicitly:

import { visuallyHidden } from '@mui/utils';

<div aria-busy={isLoading}>
  {isLoading ? (
    <React.Fragment>
      <ChatMessageSkeleton aria-hidden="true" />
      <span role="status" style={visuallyHidden}>
        Loading messages…
      </span>
    </React.Fragment>
  ) : (
    children
  )}
</div>;
  • Set aria-hidden="true" on the skeleton (extra props are forwarded to the root slot) so the shimmer lines are removed from the accessibility tree.
  • Set aria-busy on the container that swaps between skeleton and content.
  • Use a role="status" element for the announcement—it is a polite live region, so it does not interrupt the user.

The shimmer animation pauses automatically when the user requests reduced motion (prefers-reduced-motion: reduce).

Empty state

When a conversation exists but has no messages yet, ChatBox renders an empty message list area with the composer ready for input. This is the state users see when they start a new conversation.

The message area below is intentionally empty — only the conversation header and the composer render:

New conversation

Start a new conversation

No messages yet

Type a message to get started

Key characteristics of the empty state:

  • The message list area is visible but empty
  • The composer is ready for input
  • The conversation header is visible (if configured)
  • The list area collapses cleanly — no leftover dividers, scroll affordances, or placeholder rows

Custom empty state content

Provide custom empty state content by composing the thread from individual components. A common pattern is to display suggested prompts that help users start a conversation:

How can I help you today?

Streaming indicator

While the assistant is generating a response, streaming tokens are rendered incrementally inside the message bubble. The message list auto-scrolls to follow new content as long as the user is near the bottom.

The streaming state is reflected in:

  • ChatMessage.status—set to 'streaming' during generation
  • ChatTextMessagePart.state—set to 'streaming' on the active text part

Use these values to display a typing indicator or pulsing cursor:

function TypingIndicator({ message }) {
  if (message.status !== 'streaming') return null;

  return <span className="typing-cursor" />;
}

For a live, configurable streaming demo, see Streaming.

See also

API

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