Overriding components

Most parts of the SDK's UI can be replaced with a component of your own. Use this page to pick the right way for the part you want to change, and to see how each way works.

To change Do this Pattern
What the SDK renders
A component, such as the date separator, each message, the send button or the typing indicator Register your component under its key Override with <WithComponents/>
An icon Register it under the icons key. See Icons. Override with <WithComponents/>
Something the SDK looks up that isn't a component: the reactions users can pick, or the name and image shown for a user Register a value under reactionOptions, or a function under extractDisplayInfo. See Values and functions. Override with <WithComponents/>
What you render
What a <Channel/>, a <Thread/> or a layout slot shows Render other children inside it Compose children
The props of a component you render, such as a header's edge buttons or the composer's voice recording Pass other props Pass props

How WithComponents overrides work

Every replaceable part has a key in ComponentContext. When an SDK component renders a part, it looks up the part's key and renders your component if you set one, or the SDK's own otherwise. <WithComponents/> sets keys for everything below it.

Here is an App component that replaces the date separator in every message list, and the send icon everywhere it appears:

import type { DateSeparatorProps } from "stream-chat-react";
import { Chat, createIcon, WithComponents } from "stream-chat-react";
import type { PropsWithChildren } from "react";

// The list finds date separators by the class and reads their dates from `data-date`.
const PlainDateSeparator = ({ date, floating }: DateSeparatorProps) => (
  <div
    className={`str-chat__date-separator${floating ? " str-chat__date-separator--floating" : ""}`}
    data-date={date.toISOString()}
  >
    {date.toLocaleDateString()}
  </div>
);

const PaperPlaneIcon = createIcon(
  "IconSend",
  <path
    d="M3 10l14-7-5 14-2-6-7-1z"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.5"
  />,
);

// Declared once, so every render passes the same object.
const overrides = {
  DateSeparator: PlainDateSeparator,
  icons: { IconSend: PaperPlaneIcon },
};

export const App = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>
    <Chat client={client}>{children}</Chat>
  </WithComponents>
);

What happens when the app renders:

  1. <MessageList/> renders a date separator between days. It reads the DateSeparator key and finds PlainDateSeparator.
  2. The send button reads its icon through useComponentContextIcons() and finds PaperPlaneIcon under IconSend.
  3. Every other key keeps the SDK's component.

Where to put an override

Rule Why
Put <WithComponents/> above the component that reads the key A key takes effect only below it. <Chat/> itself reads NotificationAnnouncer, so that key works only from a <WithComponents/> around <Chat/>. For app-wide overrides, wrap <Chat/>.
Keep stable reference to overrides object A new object on every render gives every component that reads the context a new value, and they all re-render, each message included. Declare it at module scope, or build it with useMemo.

WithComponents and ComponentProvider

Both set ComponentContext for their subtree. They differ in what happens to the keys an outer provider set:

<WithComponents overrides={...}> <ComponentProvider value={...}>
Keys you set Replace the same keys from the providers above Replace the same keys from the providers above
Keys you don't set Keep the value from the providers above Fall back to the SDK's component
icons Merged icon by icon: the icons you don't set keep their value Replaced as a whole
Use it to Override parts Reset a subtree to the SDK's components

Nesting <WithComponents/> scopes an override to part of the app. Here is a SupportChannel component that keeps the app's date separator and also replaces the composer UI in one channel:

import {
  Channel,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";
import type { DateSeparatorProps } from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";

// The list finds date separators by the class and reads their dates from `data-date`.
const DayOnly = ({ date, floating }: DateSeparatorProps) => (
  <div
    className={`str-chat__date-separator${floating ? " str-chat__date-separator--floating" : ""}`}
    data-date={date.toISOString()}
  >
    {date.toLocaleDateString()}
  </div>
);

const SupportComposerUI = () => (
  <div className="support-composer">Replies are closed.</div>
);

const supportOverrides = { MessageComposerUI: SupportComposerUI };
const appOverrides = { DateSeparator: DayOnly };

export const SupportChannel = ({ channel }: { channel: StreamChannel }) => (
  <WithComponents overrides={appOverrides}>
    {/* This channel keeps DayOnly and replaces the composer UI too. */}
    <WithComponents overrides={supportOverrides}>
      <Channel channel={channel}>
        <MessageList />
        <MessageComposer />
      </Channel>
    </WithComponents>
  </WithComponents>
);

A custom MessageUI

The MessageUI key sets the component that renders each message in <MessageList/> and <VirtualizedMessageList/>. The VirtualMessage key, when set, replaces it in <VirtualizedMessageList/> only.

The SDK passes a custom MessageUI no props. <Message/> puts everything about the message into MessageContext, and the component reads it with useMessageContext(). The parts the default MessageUI is built from read the same context, so a custom one can compose them. Here is a CompactMessages component that renders every message in a compact form:

import {
  Attachment,
  isMessageDeleted,
  MessageActions,
  MessageText,
  MessageTimestamp,
  useMessageContext,
  WithComponents,
} from "stream-chat-react";
import type { PropsWithChildren } from "react";

const CompactMessage = () => {
  const { isMyMessage, message } = useMessageContext();

  if (isMessageDeleted(message)) {
    return (
      <div className="compact-message compact-message--deleted">
        Message deleted
      </div>
    );
  }

  return (
    <div
      className={
        isMyMessage()
          ? "compact-message compact-message--mine"
          : "compact-message"
      }
    >
      <strong>{message.user?.name ?? message.user?.id}</strong>
      <MessageText />
      {message.attachments?.length ? (
        <Attachment attachments={message.attachments} />
      ) : null}
      <MessageTimestamp />
      <MessageActions />
    </div>
  );
};

const overrides = { MessageUI: CompactMessage };

export const CompactMessages = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

MessageUI renders every regular message: deleted ones, failed ones, ones with a poll or a quoted message. The default handles all of them. A custom one handles the cases your app has. System messages go to MessageSystem instead.

The message parts and their props are on Message UI components, and a full walkthrough is the Custom message recipe.

Override keys by area

The props each key's component receives, and its default, are on ComponentContext. The page each area links to says which part of the screen each key draws.

Area Keys
Message list DateSeparator, EmptyStateIndicator, GiphyPreviewMessage, HeaderComponent, LoadingIndicator, MessageListItem, MessageListMainPanel, MessageListWrapper, MessageSystem, NewMessageNotification, ScrollToLatestMessageButton, ThreadHead, ThreadStart, TypingIndicator, UnreadMessagesNotification, UnreadMessagesSeparator
A message MessageUI, VirtualMessage, MessageActions, MessageAlsoSentInChannelIndicator, MessageBlocked, MessageBouncePrompt, MessageBubbleContent, MessageDeleted, MessageDeletedBubble, MessageEditedIndicator, MessageRepliesCountButton, MessageStatus, MessageText, MessageTimestamp, MessageTranslationIndicator, PinIndicator, QuotedMessage, ReminderNotification, StreamedMessageText, Timestamp, Avatar, AvatarStack, extractDisplayInfo
Reactions ReactionSelector, ReactionSelectorExtendedList, MessageReactions, MessageReactionsDetail, reactionOptions
Attachments and media Attachment, AttachmentFileIcon, AttachmentUploadProgressIndicator, BaseImage, FileSizeIndicator, Gallery, GalleryUI, ImagePlaceholder, ModalGallery, ProgressIndicator, UploadedSizeIndicator, VideoPlayer
Polls PollActions, PollContent, PollCreationDialog, PollCreationDialogControls, PollHeader, PollOptionSelector
Message composer MessageComposerUI, MessageComposerPreviews, TextareaComposer, AutocompleteSuggestionItem, AutocompleteSuggestionList, AttachmentSelector, AttachmentSelectorInitiationButtonContents, AttachmentPreviewList, VoiceRecordingPreviewSlot, AdditionalMessageComposerActions, AudioRecorder, StartRecordingAudioButton, RecordingPermissionDeniedNotification, CommandChip, CooldownTimer, EditedMessagePreview, QuotedMessagePreview, LinkPreviewList, SendButton, SendToChannelCheckbox, StopAIGenerationButton, EmojiPicker, FileDragAndDropContent, ShareLocationDialog
Channel list ChannelListHeader, ChannelListItemUI, ChannelListItemActionButtons, SummarizedMessagePreview, and for its pages: EmptyListIndicator, EndReachedIndicator, ListItem, LoadingNextPageIndicator. Shared with the thread list: FirstPageLoadingIndicator, ListLoadErrorIndicator (replacing them in one list only)
Search Search, SearchBar, SearchResults, SearchResultsHeader, SearchResultsPresearch, SearchSourceResultList, SearchSourceResultListFooter, SearchSourceResults, SearchSourceResultsEmpty, SearchSourceResultsHeader, SearchSourceResultsLoadingIndicator
Thread list ThreadListEmptyPlaceholder, ThreadListHeader, ThreadListItem, ThreadListItemUI, ThreadListLoadingIndicator, ThreadListUnseenThreadsBanner. Shared with the channel list: FirstPageLoadingIndicator, ListLoadErrorIndicator (replacing them in one list only)
Notifications Notification, NotificationAnnouncer, NotificationList
Dialogs and menus CalloutDialog, ContextMenu, ContextMenuContent, Modal
Icons icons

Values and functions

Besides icons, two keys take a value or a function instead of a component:

Key Takes Read by
reactionOptions The reactions users can pick, and how each one renders The reaction selector, the reactions on a message and the reactions detail
extractDisplayInfo A function that turns a user into the name and image shown for them Most avatars of a user: message authors, the reactions detail, poll votes, mention suggestions, search results, the typing indicator, thread participants and the channel details lists. The channel list's group avatars don't read it

<ChannelHeader/>, <ThreadHeader/>, <MessageList/> and <MessageComposer/> have no key: you render them yourself, so you change them by rendering something else. See Composition.

Icons

Every icon the SDK renders, from the send arrow and the delivery checkmarks to the mute and pin badges and the attachment selector's glyphs, comes from the icons key. Give it only the icons you want to change: the others keep their defaults.

Here is a ChannelListWithIcons component that replaces the mute icon:

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

const SlashedBellIcon = createIcon(
  "IconMute",
  <path
    d="M4 4l12 12M8 16h4M6 8a4 4 0 017.5-2M14 10v2l1.5 2H7"
    fill="none"
    stroke="currentColor"
    strokeLinecap="round"
    strokeWidth="1.5"
  />,
);

const iconOverrides = { icons: { IconMute: SlashedBellIcon } };

export const ChannelListWithIcons = () => (
  <WithComponents overrides={iconOverrides}>
    <ChannelList paginator={paginator} />
  </WithComponents>
);

The mute icon in that subtree, for example in a channel list item and its actions menu, is now SlashedBellIcon. The names are those of the icon components stream-chat-react exports, such as IconSend, IconReply, IconPin and IconMute, and the IconName type lists every one. File-type icons on attachments are a separate key, AttachmentFileIcon.

What an icon receives

An icon is a component of type IconComponent. It receives the props of an <svg> element, plus decorative:

Prop What to do with it
className Keep it. The SDK passes classes such as str-chat__icon--destructive.
decorative true by default: the icon is hidden from screen readers (aria-hidden). Pass false when the icon is the only label of a control.
The rest of the SVG props Pass them on, such as aria-label or style.

The SDK's own icons have a 0 0 20 20 view box, mostly draw with currentColor, and carry the str-chat__icon class and a class named after the icon, such as str-chat__icon--mute. Stylesheets and theming variables size and color icons through those classes.

Building an icon

createIcon(name, content) builds an icon the way the SDK builds its own: it wraps content in an <svg> with the view box, the classes and the accessibility attributes in What an icon receives, and forwards the props it receives. content is the inside of the SVG, drawn for a 20 by 20 box.

An icon doesn't have to be an SVG. Any component of type IconComponent works, for example an image or an icon from another library. Here is a LogoSendIcon component that shows an image:

import clsx from "clsx";
import type { IconComponent } from "stream-chat-react";

export const LogoSendIcon: IconComponent = ({
  className,
  decorative = true,
}) => (
  <img
    alt=""
    aria-hidden={decorative}
    className={clsx("str-chat__icon", className)}
    src="/logo-send.svg"
  />
);

Keep className and the str-chat__icon class, so the SDK's stylesheet still sizes the icon. Keep icons light: one may render once per message.

Inheritance

Icon overrides merge icon by icon across nested <WithComponents/>. Set app-wide icons around <Chat/>, and override one icon deeper down where a part of the app needs it. Here is an App component with a brand send icon everywhere and another one in an escalations channel:

import {
  Channel,
  Chat,
  createIcon,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";

const BrandSendIcon = createIcon(
  "IconSend",
  <circle cx="10" cy="10" r="7" fill="currentColor" />,
);
const UrgentSendIcon = createIcon(
  "IconSend",
  <rect x="3" y="3" width="14" height="14" fill="currentColor" />,
);

const brandIcons = { icons: { IconSend: BrandSendIcon } };
const urgentIcons = { icons: { IconSend: UrgentSendIcon } };

export const App = ({ escalations }: { escalations: StreamChannel }) => (
  <WithComponents overrides={brandIcons}>
    <Chat client={client}>
      {/* This channel uses UrgentSendIcon; every other icon is inherited. */}
      <WithComponents overrides={urgentIcons}>
        <Channel channel={escalations}>
          <MessageList />
          <MessageComposer />
        </Channel>
      </WithComponents>
    </Chat>
  </WithComponents>
);

Reading the icons in your own components

useComponentContextIcons() returns every icon, with the overrides in effect merged over the defaults. A component that uses it shows the same icons as the SDK's. Here is a ShareButton component that uses the SDK's arrow icon:

import { useComponentContextIcons } from "stream-chat-react";

export const ShareButton = ({ onClick }: { onClick: () => void }) => {
  const { IconArrowUpRight } = useComponentContextIcons();

  return (
    <button aria-label="Share" onClick={onClick} type="button">
      <IconArrowUpRight />
    </button>
  );
};

Composition

Not every change needs a key in ComponentContext. <Channel/>, <Thread/> and the layout's slots render no UI of their own, only the children you give them, so you choose what they show: leave a part out, add your own, or change the order. Here is a ModeratedChannel component that adds a rules banner under the header:

import {
  Channel,
  ChannelHeader,
  MessageComposer,
  MessageList,
} from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";

const RulesBanner = () => (
  <div className="rules-banner">Be kind. Moderators are watching.</div>
);

export const ModeratedChannel = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel}>
    <ChannelHeader />
    <RulesBanner />
    <MessageList />
    <MessageComposer />
  </Channel>
);

Component props

The components you render take props that change what they show. Each component's page in the Component reference lists its props, for example MessageList and MessageComposer. For the headers, The header in Channel UI and The thread header in Thread UI show which part each prop replaces.

Pitfalls

Symptom Cause Fix
Every message re-renders on each render of your app An overrides object created on every render Keep stable reference
Overrides set higher up stop working in a subtree <ComponentProvider/> used where you meant <WithComponents/>: it drops the keys of every provider above it Use <WithComponents/>
A NotificationAnnouncer override has no effect It's set below <Chat/>, which reads it Set it in a <WithComponents/> around <Chat/>
A MessageStatus override that requires props, such as ({ label }: { label: string }) => …, is a type error The SDK renders the MessageStatus key without props Render the exported component with its props inside the override: MessageStatus: () => <MessageStatus MessageReadStatus={ReadIcon} />
Jumping to a message doesn't reach messages your MessageListItem renders The jump finds a message by the data-message-id in the MessageListItemProps, and your element doesn't carry them; see Message list UI Spread the props onto the element: ({ children, ...props }) => <li {...props}>{children}</li>
An override, such as LoadingIndicator, shows in a place where you want the default The SDK reads the key in most places it renders that part Wrap that place in a <WithComponents/> with the exported default, in an object declared once at module scope: const keepDefaultSpinner = { LoadingIndicator };, then overrides={keepDefaultSpinner}
A custom MessageUI gets no message It expects props, and it receives none Read useMessageContext()
A MessageUI change doesn't show in <VirtualizedMessageList/> VirtualMessage is set, and it wins there Change VirtualMessage too, or unset it
An AttachmentSelector override also replaces the upload button of an inline editor In a composer rendered inside a message, the SDK uses a simpler selector that only uploads files. A component set on the key replaces both. Inside a message, render the exported <SimpleAttachmentSelector/> from your component

Used in