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. AnEmptyStateIndicatoroverride 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, andTypingIndicatorHeader, in the header, as two separate parts. - To change every spinner at once, replace the
IconLoadingicon:LoadingIndicatorrenders 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>
);