Indicators

The SDK's loading, error, empty-state, pagination and typing indicators. This page lists where each one renders, how to replace it, and its props. The Components pages show them in context: List states UI, Message list UI and Channel UI.

Best practices

  • Replace an indicator through its override key, with WithComponents. The lists don't take indicators as props.
  • The channel list has keys of its own, such as EmptyListIndicator. An EmptyStateIndicator override doesn't reach it.
  • Keep a loading or empty state the size of what it stands in for, so the layout doesn't jump when the data arrives.
  • Treat TypingIndicator, at the bottom of the message list, and TypingIndicatorHeader, in the header, as two separate parts.
  • To change every spinner at once, replace the IconLoading icon: LoadingIndicator renders it.

Available components

Every component here imports from stream-chat-react.

Component What it shows Where the SDK renders it Override key
EmptyStateIndicator A message for an empty list MessageList and VirtualizedMessageList with no messages. The channel list's default empty state. EmptyStateIndicator, for the message lists
ListLoadErrorIndicator An icon, "Couldn't load conversations" or "Couldn't load threads", and a "Try again" button The channel list and the thread list, in place of the list when its first page failed to load ListLoadErrorIndicator, for both lists
LoadingChannels Five placeholder rows The channel list and the thread list, while they load their first page FirstPageLoadingIndicator, for both lists
LoadingChannel A placeholder header, three messages and a composer Nowhere. Render it yourself while a channel loads. None
LoadingErrorIndicator "Error:" and the error's message Nowhere None
LoadingIndicator A spinner The message lists while they load a page, the thread list's next page, upload previews, and other spinners LoadingIndicator, for the parts listed below
LoadMoreButton A "Load more" button, with a spinner while loading Only inside LoadMorePaginator None
LoadMorePaginator Its children and a LoadMoreButton Nowhere. The channel list loads its next page on scroll. None
TypingIndicator The avatars of the people typing and animated dots The bottom of MessageList, and of VirtualizedMessageList when the key is set TypingIndicator
TypingIndicatorHeader "Cleo is typing" and animated dots The subtitle of ChannelHeader and ThreadHeader None

UI customization

EmptyStateIndicator

The message lists render the EmptyStateIndicator override with listType="message" in a channel and listType="thread" in a thread. The default shows "Send a message to start the conversation", or messageText when passed, and nothing in a thread.

The other lists have their own keys:

  • the channel list renders EmptyListIndicator, which receives no props and defaults to <EmptyStateIndicator listType="channel" />, "No conversations yet";
  • the thread list renders ThreadListEmptyPlaceholder.

FirstPageLoadingIndicator and ListLoadErrorIndicator

FirstPageLoadingIndicator gets no props and defaults to LoadingChannels. ListLoadErrorIndicator gets ListLoadErrorIndicatorProps. Its default is the exported <ListLoadErrorIndicator/>, with listType set to "channel" or "thread". List states UI shows what each list renders and what retry calls.

While a retry runs, the list keeps the error view on screen with isRetrying set to true, so a focused "Try again" button isn't swapped out and keeps the focus. The default button sets aria-busy and aria-disabled meanwhile, not disabled, which would take the focus away. When the retry fails, the view stays and gets the new error as error. When it loads the first page, screen readers hear "Conversations loaded" or "Threads loaded".

The view's title and the list's error notification read the same translation key, channelList.loadError.text or threadList.loadError.text, so overriding that key changes both.

LoadingErrorIndicator

LoadingErrorIndicator renders "Error: " and error.message, and nothing without an error. The SDK doesn't render it, and it has no override key. Render it yourself where a load of your own can fail. A channel list or thread list whose first page failed renders ListLoadErrorIndicator instead: see FirstPageLoadingIndicator and ListLoadErrorIndicator.

LoadingIndicator

These parts render the LoadingIndicator override: VirtualizedMessageList while it loads a page of messages, MessageList while it loads an older page (its loadingMore prop changes when), the thread list's default ThreadListLoadingIndicator, an attachment preview while the server confirms its upload, and a video attachment while its player loads. The SDK passes it no props that change how it looks.

The key also reaches <LoadMoreButton/>, the unseen threads banner, the poll answer and vote lists, the image gallery's thumbnails while they load, the lists in channel details, and the reactions dialog while it loads the users who reacted. That dialog's default is MessageReactionsDetailLoadingIndicator, a skeleton of three rows, not a spinner. The channel list's next page is the exception: its default LoadingNextPageIndicator renders the SDK's LoadingIndicator, so replace LoadingNextPageIndicator there. Every spinner, through the key or not, renders the IconLoading icon, so replacing that icon changes every spinner. Icons shows how.

LoadMorePaginator and LoadMoreButton

LoadMorePaginator renders its children and, while hasNextPage is true, a LoadMoreButton that calls loadNextPage. Use them for a list of your own. ChannelList has no paginator prop: it loads the next page when it's scrolled to the bottom, and shows LoadingNextPageIndicator meanwhile.

TypingIndicator

MessageList passes the TypingIndicator override isMessageListScrolledToBottom and scrollToBottom. It shows only while the list is scrolled to the latest message, and scrolls the list down when it appears. VirtualizedMessageList has no default: it renders a typing indicator only when the TypingIndicator key is set.

It reads channel.state.typing, leaves out the current user, and renders nothing when typing events are off for the channel. Inside a Thread, it shows the people typing a reply in that thread. Typing indicators covers the feature end to end.

TypingIndicatorHeader

TypingIndicatorHeader replaces the subtitle of ChannelHeader and ThreadHeader while someone is typing. It has no override key: a custom header or a custom Subtitle renders it itself. It reads channel.state.typing. Without threadList, it shows typing in the channel. With threadList, it shows typing in the replies of the Thread around it.

EmptyStateIndicatorProps

Prop Type Default Description
listType "channel" | "message" | "thread" The list it's shown in. With no listType, the default shows "No items exist".
messageText string For listType="message", the text to show instead of the default. It's passed through translation.

ListLoadErrorIndicatorProps

The props the lists pass to the ListLoadErrorIndicator override.

Prop Type Default Description
error Error required The error of the failed query.
isRetrying boolean required true while a retry runs. The list keeps the component on screen meanwhile.
retry () => void required Queries the first page again. Does nothing while a retry runs.

The exported <ListLoadErrorIndicator/> takes DefaultListLoadErrorIndicatorProps: these props and listType, "channel" or "thread", which picks its text. To render it inside an override, pass the props on and set listType.

The default view has these classes:

Class Element
str-chat__list-load-error The root, in both lists
str-chat__channel-list-load-error The root, in the channel list
str-chat__thread-list-load-error The root, in the thread list
str-chat__list-load-error__icon The icon
str-chat__list-load-error__content The wrapper of the title and the button
str-chat__list-load-error__title The title, "Couldn't load conversations" or "Couldn't load threads"
str-chat__list-load-error__retry-button The "Try again" button

LoadingErrorIndicatorProps

Prop Type Default Description
error Error | null The error to show. Without one, the indicator renders nothing.

LoadingIndicatorProps

LoadingIndicatorProps are the props of an SVG element, plus:

Prop Type Default Description
decorative boolean true Hides the spinner from assistive technology and keeps it out of the tab order.

It passes them to the IconLoading icon, with the class str-chat__loading-indicator in place of any className you pass.

LoadMoreButtonProps

Prop Type Default Description
onClick React.MouseEventHandler<HTMLButtonElement> required Loads the next page.
isLoading boolean Disables the button and shows a spinner instead of its label.
children ReactNode "Load more" The label.

LoadMorePaginatorProps

Prop Type Default Description
loadNextPage () => void required Called by the button.
hasNextPage boolean Shows the button while true.
isLoading boolean Passed to the button.
LoadMoreButton React.ComponentType<LoadMoreButtonProps> LoadMoreButton The button to render.
reverse boolean Puts the button before the children instead of after them.
children ReactNode The list.

LoadMorePaginatorProps also accepts hasPreviousPage, loadPreviousPage, LoadingIndicator and threshold, from the shared PaginatorProps type, but LoadMorePaginator doesn't read them.

TypingIndicatorProps

Prop Type Default Description
isMessageListScrolledToBottom boolean true When false, the indicator renders nothing.
scrollToBottom () => void Called when the indicator appears while the list is scrolled to the bottom.

Whether it shows typing in the channel or in a thread depends on whether it renders inside a Thread, not on a prop.

TypingIndicatorHeaderProps

Prop Type Default Description
threadList boolean false Shows typing in the replies of the surrounding Thread instead of in the channel.

Examples

Replace the message list's empty state

import { Channel, MessageList, WithComponents } from "stream-chat-react";
import type { EmptyStateIndicatorProps } from "stream-chat-react";

const EmptyChannel = ({ listType }: EmptyStateIndicatorProps) =>
  listType === "message" ? (
    <p className="app-empty-channel">No messages yet. Say hi!</p>
  ) : null;

export const App = () => (
  <WithComponents overrides={{ EmptyStateIndicator: EmptyChannel }}>
    <Channel channel={channel}>
      <MessageList />
    </Channel>
  </WithComponents>
);

Returning null for "thread" keeps a thread's list empty, as the default does.

Replace the thread list's load error

import { ThreadList, WithComponents } from "stream-chat-react";
import type { ListLoadErrorIndicatorProps } from "stream-chat-react";

const ThreadsLoadError = ({
  isRetrying,
  retry,
}: ListLoadErrorIndicatorProps) => (
  <div className="app-threads-error">
    <p>Your threads didn't load.</p>
    <button
      aria-busy={isRetrying}
      aria-disabled={isRetrying}
      onClick={retry}
      type="button"
    >
      Try again
    </button>
  </div>
);

const overrides = { ListLoadErrorIndicator: ThreadsLoadError };

export const Threads = () => (
  <WithComponents overrides={overrides}>
    <ThreadList />
  </WithComponents>
);

The override wraps <ThreadList/> only, so the channel list keeps its default error view.

Replace the channel list's empty state

import { ChannelList, WithComponents } from "stream-chat-react";

const NoChannels = () => (
  <p className="app-no-channels">Join a channel to start.</p>
);

export const Channels = () => (
  <WithComponents overrides={{ EmptyListIndicator: NoChannels }}>
    <ChannelList paginator={paginator} />
  </WithComponents>
);

Show typing in VirtualizedMessageList

import {
  Channel,
  TypingIndicator,
  VirtualizedMessageList,
  WithComponents,
} from "stream-chat-react";

export const App = () => (
  <WithComponents overrides={{ TypingIndicator }}>
    <Channel channel={channel}>
      <VirtualizedMessageList />
    </Channel>
  </WithComponents>
);