Overriding components
Every replaceable part of the UI 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.
For example, this 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";
const PlainDateSeparator = ({ date }: DateSeparatorProps) => (
<div className="str-chat__date-separator">{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>
);MessageListrenders a date separator between days. It reads theDateSeparatorkey and findsPlainDateSeparator.- The send button reads its icon through
useComponentContextIcons()and findsPaperPlaneIconunderIconSend. - Every other key keeps the SDK's component.
WithComponents vs ComponentProvider
Both set ComponentContext for their subtree. They differ in what happens to the keys an outer provider set:
WithComponentsmerges. The keys inoverridesreplace the same keys from anyWithComponentsabove it, and every other key keeps its outer value. Theiconskey merges icon by icon, so overriding one icon keeps the icons an outer provider set.ComponentProviderreplaces. Itsvaluebecomes the whole context: a key it doesn't set falls back to the SDK's component, even if an outer provider set it. Use it to reset a subtree to the defaults.
Nesting WithComponents scopes an override to part of the app:
import {
Channel,
MessageComposer,
MessageList,
WithComponents,
} from "stream-chat-react";
import type { DateSeparatorProps } from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";
const DayOnly = ({ date }: DateSeparatorProps) => (
<div className="str-chat__date-separator">{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>
);Where to put it:
- Above the component that reads the key.
Chatitself readsNotificationAnnouncer, so that key only takes effect from aWithComponentsaroundChat. For app-wide overrides, wrapChat. - With a stable
overridesobject. A new object on every render gives every component that reads the context a new value, and they all re-render. Declare it at module scope, as above, or build it withuseMemo.
Override keys by area
The props each key's component receives, and its default, are on ComponentContext. The Components pages say which part of the screen each key draws.
- Message list:
DateSeparator,EmptyStateIndicator,GiphyPreviewMessage,HeaderComponent,LoadingIndicator,MessageListItem,MessageListMainPanel,MessageListWrapper,MessageSystem,NewMessageNotification,ThreadHead,ThreadStart,TypingIndicator,UnreadMessagesNotification,UnreadMessagesSeparator. - A message:
MessageUI,VirtualMessage,MessageActions,MessageAlsoSentInChannelIndicator,MessageBlocked,MessageBouncePrompt,MessageDeleted,MessageDeletedBubble,MessageEditedIndicator,MessageRepliesCountButton,MessageStatus,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,PollHeader,PollOptionSelector. - Message composer:
MessageComposerUI,TextareaComposer,AutocompleteSuggestionItem,AutocompleteSuggestionList,AttachmentSelector,AttachmentSelectorInitiationButtonContents,AttachmentPreviewList,VoiceRecordingPreviewSlot,AdditionalMessageComposerActions,AudioRecorder,StartRecordingAudioButton,RecordingPermissionDeniedNotification,CommandChip,CooldownTimer,EditedMessagePreview,QuotedMessagePreview,LinkPreviewList,SendButton,SendToChannelCheckbox,StopAIGenerationButton,EmojiPicker,emojiSearchIndex,ShareLocationDialog. - Channel list:
ChannelListHeader,ChannelListItemUI,ChannelListItemActionButtons,SummarizedMessagePreview,HeaderEndContent, and for its pages:EmptyListIndicator,EndReachedIndicator,FirstPageLoadingIndicator,ListItem,LoadingNextPageIndicator. - Search:
Search,SearchBar,SearchResults,SearchResultsHeader,SearchResultsPresearch,SearchSourceResultList,SearchSourceResultListFooter,SearchSourceResults,SearchSourceResultsEmpty,SearchSourceResultsHeader,SearchSourceResultsLoadingIndicator. - Thread list:
ThreadListEmptyPlaceholder,ThreadListItem,ThreadListItemUI,ThreadListLoadingIndicator,ThreadListUnseenThreadsBanner. - Notifications:
Notification,NotificationAnnouncer,NotificationList. - Dialogs and menus:
CalloutDialog,ContextMenu,ContextMenuContent,Modal. - Icons:
icons.
extractDisplayInfo, emojiSearchIndex and reactionOptions take a function or data, not a component.
The ComponentContext type also lists FileDragAndDropContent and ScrollToLatestMessageButton, but no component reads them, so setting them changes nothing.
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.
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 row 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:
className. The SDK passes classes such asstr-chat__icon--destructive. Keep them.decorative.trueby default: the icon is hidden from screen readers (aria-hidden). Passfalsewhen the icon is the only label of a control.- The rest of the SVG props, such as
aria-labelorstyle.
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 above, 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:
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:
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 custom component that uses it shows the same icons as the SDK's:
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>
);
};A custom MessageUI
MessageUI renders each message in MessageList and VirtualizedMessageList. VirtualMessage, 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:
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 messageOverrides = { MessageUI: CompactMessage };
export const CompactMessages = ({ children }: PropsWithChildren) => (
<WithComponents overrides={messageOverrides}>{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, and 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.
Composition
Not every change needs a key. Channel, Thread and the layout's slots render no UI of their own, only the children you give them, so their content is yours to compose: leave a part out, add your own, or change the order.
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>
);Header content
ChannelHeader and ThreadHeader take two slots:
StartContent, at the start. It defaults toWorkspaceNavigationBackButton, the back button of a panel opened on top of another.EndContent, at the end. It defaults toWorkspaceNavigationCloseButton, the close button of a panel that can be closed.
A component you pass replaces the default. To keep the default button, render it yourself:
import {
ChannelHeader,
useChannel,
WorkspaceNavigationCloseButton,
} from "stream-chat-react";
const HeaderEnd = () => {
const channel = useChannel();
return (
<>
<a href={`/calls/${channel.id}`} rel="noreferrer" target="_blank">
Call
</a>
<WorkspaceNavigationCloseButton />
</>
);
};
export const CallableChannelHeader = () => (
<ChannelHeader EndContent={HeaderEnd} />
);ChannelHeader also takes Avatar, title and image. The headers of the channel list and the thread list have a key instead: HeaderEndContent, rendered at the end of the channel list's header while a channel is open, and of the thread list's while a thread is open.
The Channel header and Thread header recipes build complete headers.
Pitfalls
- An
overridesobject created on every render. It re-renders every component that readsComponentContext, including each message. Declare it once. ComponentProviderwhere you meantWithComponents. It drops the overrides of every provider above it.- An override below
Chatfor a keyChatreads.NotificationAnnouncertakes effect only from aboveChat. - A custom
MessageUIthat expects props. It receives none: readuseMessageContext(). VirtualMessageset, and aMessageUIchange that doesn't show. InVirtualizedMessageList,VirtualMessagewins.- An
AttachmentSelectoroverride in the edit composer. While a message is being edited, the SDK uses a simpler selector that only uploads files. A component set on theAttachmentSelectorkey replaces both.
Used in: most Features pages and every recipe, for example Custom message and Custom composer.