Theming
The SDK's look comes from a theme: a JavaScript object with the design tokens the components color themselves with, and a style object for each component. You pass a partial theme, and the SDK merges it over its default. Setting one token changes everything that reads it: chatBgOutgoing alone recolors the bubbles of the user's own messages in every list. Replace a component through WithComponents when you need a different structure; for colors, spacing, sizes and fonts, change the theme.
Setting the theme
OverlayProvider takes the theme as value.style, and Chat as style. Pass the same object to both:
import type { PropsWithChildren } from "react";
import type { StreamChat } from "stream-chat";
import { Chat, OverlayProvider } from "stream-chat-react-native";
import type { DeepPartial, Theme } from "stream-chat-react-native";
// Declared once, at module scope.
const theme: DeepPartial<Theme> = {
channelPreview: {
title: { fontSize: 17 },
},
semantics: {
accentPrimary: "#0d47a1",
buttonPrimaryBg: "#0d47a1",
chatBgOutgoing: "#1e3a8a",
chatTextOutgoing: "#ffffff",
textLink: "#1e40af",
},
};
export const App = ({
children,
client,
}: PropsWithChildren<{ client: StreamChat }>) => (
<OverlayProvider value={{ style: theme }}>
<Chat client={client} style={theme}>
{children}
</Chat>
</OverlayProvider>
);OverlayProvidermerges the theme over the default, and gives it to everything below it, the message menu's background and the image gallery included.Chatdoes the same for everything below it. Without its ownstyle,ChattakesOverlayProvider's theme. With one, it merges thatstyleover the default, not overOverlayProvider's, which is why the two get the same object.- Every component reads the merged theme with
useTheme().
DeepPartial<Theme> makes every key optional, at every depth, so you set only what you change. The merge is deep: channelPreview.title.fontSize keeps every other style of the title.
The theme is merged again when the object changes, so declare it at module scope, or build it with useMemo, as below for dark mode. The full shape of Theme is on Theme object, and its defaults are in package/src/contexts/themeContext/utils/theme.ts in the SDK's repository.
Design tokens
The tokens come in tiers, and each tier is built from the one above it:
- Foundations. Raw values: colors such as
blue500andslate100, and the scales of sizes, spacing, radii and type. - Primitives. Values for a purpose that doesn't change with the color scheme: spacing (
spacingMd), radii (radiusLg), type sizes and weights (typographyFontSizeMd), and shadows (lightElevation1). - Semantic tokens. What a color is for:
accentPrimary,textPrimary,backgroundCoreApp,borderCoreDefault, and the colors of one kind of component, such aschatBgOutgoingfor the bubbles of the user's own messages,avatarBgDefaultandbuttonPrimaryBg. There is a light set and a dark set. - Component values. Sizes for one part of one component, such as the corner radius of a message bubble or the padding of a button.
The SDK generates the tokens from its design files, with values for each platform: the type scale on iOS and on Android follows each platform's own.
Only the semantic tokens are part of the theme, under semantics. The other tiers are constants the components use directly, so you change what they control through the per-component styles below.
Changing colors
Set semantic tokens under semantics, as the first example does. The SDK resolves each token from the tier above it, accentPrimary from brand500 for example, before it merges your theme. So a token you set changes exactly the components that read that token, and nothing built from it:
- Setting
accentPrimaryrecolors what readsaccentPrimary: the read receipts, the selection circles, the loading spinners and upload progress, and poll options. The primary buttons readbuttonPrimaryBgand the badgesbadgeBgPrimary, both already resolved from the default accent, so set those too, as the example does for the buttons. - Setting
brand500doesn't recolor the accent, the links or the bubbles, because they were resolved from it before your theme was merged. Almost no component reads a brand step directly (the member role badge in channel details readsbrand150andbrand900), so to recolor the UI, set the semantic tokens themselves. - A value is used as you write it. A
$-reference such as"$brand500"isn't resolved in your theme.
To find the tokens one part uses, look in its component's source for semantics.. The tokens and their default values are listed on Design tokens.
Changing one component
Most components have a key in the theme, named after them, with a style for each of their parts: channelPreview.title, messageComposer.inputBoxContainer, inlineDateSeparator.container. A component spreads the style over its own, so a property you set wins over the default, and the properties you leave out keep it.
import type { DeepPartial, Theme } from "stream-chat-react-native";
export const theme: DeepPartial<Theme> = {
channelPreview: {
title: { color: "#bf360c", fontWeight: "700" },
},
inlineDateSeparator: {
container: { borderRadius: 4 },
},
messageItemView: {
content: {
timestampText: { fontSize: 11 },
},
},
};Most values are React Native styles: ViewStyle, TextStyle or ImageStyle. Some are not: icon entries take IconProps, such as channelPreview.mutedStatus; messageItemView.content.markdown takes the styles of the Markdown renderer, one TextStyle or ViewStyle for each Markdown element; and a few are plain numbers, such as screenPadding. Markdown styles lists the Markdown elements and what each key styles.
The top-level keys are: aiTypingIndicatorView, attachmentPicker, attachmentSelectionBar, audioAttachment, avatar, avatarStack, bottomSheetModal, channel, channelDetails, channelDetailsMenu, channelListFooterLoadingIndicator, channelListHeaderErrorIndicator, channelListLoadingIndicator, channelListSkeleton, channelListView, channelPreview, dateHeader, emptyList, emptySearchResult, emptyStateIndicator, fileAttachmentListSkeleton, groupAvatar, iconBadge, iconSquare, imageGallery, inlineDateSeparator, loadingDots, loadingErrorIndicator, loadingIndicator, memberListSkeleton, messageComposer, messageItemView, messageList, messageMenu, messagePreview, notification, notificationList, pinnedMessageListSkeleton, poll, progressControl, reply, screenPadding, selectionCircle, semantics, spinner, thread, threadListItem, threadListSkeleton, threadListUnreadBanner, typingIndicator, userListSkeleton and waveProgressBar. Each one's parts are on Theme object.
Fonts
The SDK's components set no font family, apart from a monospace font for Markdown code blocks, so their text renders in the platform's system font. To use your app's font, set fontFamily on the text styles you want it in, such as channelPreview.title and messageItemView.content.markdown.text. The font has to be loaded by your app first.
Light and dark
The theme follows the device's color scheme. useColorScheme() decides which set of semantic tokens the SDK starts from, and the UI switches when the user changes the setting.
Your semantics apply to whichever set is active. To use different values in each scheme, pick the theme by the scheme:
import { useColorScheme } from "react-native";
import type { PropsWithChildren } from "react";
import type { StreamChat } from "stream-chat";
import { Chat, OverlayProvider } from "stream-chat-react-native";
import type { DeepPartial, Theme } from "stream-chat-react-native";
const lightTheme: DeepPartial<Theme> = {
semantics: {
accentPrimary: "#0d47a1",
buttonPrimaryBg: "#0d47a1",
chatBgOutgoing: "#dbeafe",
},
};
const darkTheme: DeepPartial<Theme> = {
semantics: {
accentPrimary: "#4a90e2",
buttonPrimaryBg: "#4a90e2",
chatBgOutgoing: "#1e3a8a",
},
};
export const App = ({
children,
client,
}: PropsWithChildren<{ client: StreamChat }>) => {
const scheme = useColorScheme();
const theme = scheme === "dark" ? darkTheme : lightTheme;
return (
<OverlayProvider value={{ style: theme }}>
<Chat client={client} style={theme}>
{children}
</Chat>
</OverlayProvider>
);
};Both themes are module-scope objects, so the theme changes only when the scheme does. Component styles you set apply in both schemes, so set colors through semantics rather than in component styles, or give each scheme its own component styles too.
The SDK has no prop that fixes the scheme. To keep the app light whatever the device's setting, call React Native's Appearance.setColorScheme("light"), which useColorScheme() follows.
Reading the theme in your own components
useTheme() returns { theme }, the merged theme of the nearest provider. A component that colors itself from it follows your theme and the color scheme like the SDK's own:
import { StyleSheet, Text, View } from "react-native";
import { useTheme } from "stream-chat-react-native";
export const NewBadge = () => {
const {
theme: { semantics },
} = useTheme();
return (
<View style={[styles.badge, { backgroundColor: semantics.badgeBgPrimary }]}>
<Text style={{ color: semantics.badgeTextOnAccent }}>New</Text>
</View>
);
};
const styles = StyleSheet.create({
badge: { borderRadius: 8, paddingHorizontal: 6 },
});useTheme() throws outside OverlayProvider and Chat. Read the theme each render rather than caching it in a ref or in module state, so the component updates when the theme or the scheme changes.
The user's own messages
myMessageTheme on Channel and Thread is a second partial theme, merged over the app's theme for the current user's messages only, and their date separators:
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
import type { DeepPartial, Theme } from "stream-chat-react-native";
const myMessageTheme: DeepPartial<Theme> = {
messageItemView: {
content: {
timestampText: { fontStyle: "italic" },
},
},
};
export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
<Channel channel={channel} myMessageTheme={myMessageTheme}>
<MessageList />
<MessageComposer />
</Channel>
);The bubble colors of the user's own messages already have their own tokens, chatBgOutgoing and chatTextOutgoing, so use myMessageTheme for what has no such token. The message list merges it once, and again only when its content changes. forceAlignMessages on Channel puts every message on one side instead.
Right-to-left layouts
The SDK follows React Native's layout direction. When I18nManager.isRTL is true, the layout mirrors, and so do the icons that point in a direction, such as the reply and send arrows. I18nManager.forceRTL(true) takes effect after the app restarts.
Used in
- Quick start, whose
OverlayProviderandChattake the theme. - UI Cookbooks: when the theme isn't enough, a UI cookbook replaces the component, for example Incoming and outgoing styles.
Pitfalls
- A theme on
Chatonly, or onOverlayProvideronly.Chatwith its ownstyledoesn't build onOverlayProvider's, andOverlayProvidernever seesChat's. Pass the same object to both. - Setting a token and expecting the tokens built from it to follow. Tokens are resolved before your theme is merged. Set every token the parts you want to change read:
accentPrimaryandbuttonPrimaryBg, for example. - A
$-reference in your theme. It isn't resolved, and renders as an invalid color. Write the value. - A theme object created on every render. Each new object merges the whole theme again. Declare it at module scope, or memoize it.
- Colors in component styles with dark mode. A color you set in a component style shows in both schemes. Use
semantics, or pick the theme by the scheme. useTheme()outside the providers. It throws. Render your component belowOverlayProviderorChat.