import { Chat, OverlayProvider } from "stream-chat-react-native";
const theme = {
notification: {
container: {
backgroundColor: "#1f2937",
borderRadius: 16,
paddingHorizontal: 16,
paddingVertical: 12,
},
message: {
color: "#f9fafb",
fontSize: 14,
},
actionButton: {
backgroundColor: "#374151",
},
closeButton: {
backgroundColor: "transparent",
},
iconContainer: {
marginRight: 4,
},
},
notificationList: {
container: {
// Bump the snackbar above a custom bottom toolbar
bottom: 96,
},
},
};
export const App = () => (
<OverlayProvider value={{ style: theme }}>
<Chat client={client}>{/* ... */}</Chat>
</OverlayProvider>
);Snackbar
The SDK renders in-app notifications as a snackbar: a single floating card mounted by NotificationList and rendered by Notification. This cookbook walks through customizing how that snackbar looks and behaves, from the lightest touch (theme tokens) to a full replacement (your own snackbar component on top of the SDK's notification store).
If you only need to know how to emit notifications, see the In-App Notifications cookbook instead.
Best Practices
- Prefer theme tokens. The vast majority of brand-matching tweaks (colors, spacing, typography) need no overrides.
- Mount
NotificationListonce per active target.MessageList,ChannelList, andThreadListalready do this in the built-in layouts. Only mount your own when you've replaced those default child components. - When overriding
Notification, provide it throughWithComponentsand preserve the SDK's accessibility props: live-region for errors (assertive) vs other severities (polite),accessibilityRole='alert'for errors and'summary'otherwise, and a translatableaccessibilityLabelon the close button. - Don't mix custom and built-in snackbars at the same time. Pick one extension level and stick with it per app.
- If you need entirely different motion (e.g. top-of-screen banners instead of bottom snackbar), replace
NotificationListrather than fighting the built-in animations.
Level 1: Theme Tokens
The fastest customization is restyling. The theme exposes per-component slots for the snackbar:
Available slots:
| Slot | Type | What it styles |
|---|---|---|
notification.container | ViewStyle | The snackbar card |
notification.contentContainer | ViewStyle | Icon + message wrapper |
notification.iconContainer | ViewStyle | Icon bubble |
notification.message | TextStyle | Message text |
notification.actionsContainer | ViewStyle | Wrapper around action buttons |
notification.actionButton | ViewStyle | Each action button |
notification.closeButton | ViewStyle | Dismiss button |
notificationList.container | ViewStyle | The absolute-positioned host |
Level 2: Override the Icon
To change the severity-to-icon mapping without touching the rest of the snackbar, override NotificationIcon via WithComponents so every Notification instance picks it up:
import { Chat, useTheme, WithComponents } from "stream-chat-react-native";
import type { NotificationIconProps } from "stream-chat-react-native";
import { CustomError, CustomCheck, CustomInfo, CustomWarning } from "./icons";
const IconBySeverity = {
error: CustomError,
success: CustomCheck,
warning: CustomWarning,
info: CustomInfo,
} as const;
const CustomNotificationIcon = ({ notification }: NotificationIconProps) => {
const {
theme: { semantics },
} = useTheme();
const severity = notification.severity;
const Icon = severity
? IconBySeverity[severity as keyof typeof IconBySeverity]
: null;
if (!Icon) return null;
return <Icon height={20} width={20} color={semantics.textOnInverse} />;
};
<WithComponents overrides={{ NotificationIcon: CustomNotificationIcon }}>
<Chat client={client}>{/* ... */}</Chat>
</WithComponents>;Level 3: Override the Notification Component
To restructure the snackbar, for example with a different layout, custom close gesture, or brand-specific motion, replace Notification. The component receives the notification object plus these optional props:
| Prop | Type | Description |
|---|---|---|
notification | Notification | The notification to render. Always present. |
Icon | ComponentType<NotificationIconProps> | The resolved icon component (defaults to the NotificationIcon provided through WithComponents). |
onDismiss | () => void | Call this to dismiss the active notification. NotificationList removes it from the store. |
showClose | boolean | Whether to render the close button. NotificationList sets this to true for persistent notifications. |
entryDirection | 'bottom' | 'left' | 'right' | 'top' | The animation direction the host picked. Useful for stacked transforms. |
import { Text, View } from "react-native";
import { Pressable as GHPressable } from "react-native-gesture-handler";
import {
Chat,
useTheme,
useTranslationContext,
WithComponents,
} from "stream-chat-react-native";
import type { NotificationProps } from "stream-chat-react-native";
const CustomNotification = ({
Icon,
notification,
onDismiss,
showClose,
}: NotificationProps) => {
const {
theme: { semantics },
} = useTheme();
const { t } = useTranslationContext();
const isError = notification.severity === "error";
const closeVisible = showClose || !notification.duration;
return (
<View
accessibilityLiveRegion={isError ? "assertive" : "polite"}
accessibilityRole={isError ? "alert" : "summary"}
style={{
backgroundColor: semantics.backgroundCoreInverse,
borderRadius: 16,
flexDirection: "row",
padding: 12,
}}
>
{Icon ? <Icon notification={notification} /> : null}
<Text style={{ color: semantics.textOnInverse, flex: 1, marginLeft: 8 }}>
{t(notification.message)}
</Text>
{notification.actions?.map((action, i) => (
<GHPressable
accessibilityRole="button"
key={`${action.label}-${i}`}
onPress={action.handler}
style={{ marginLeft: 8 }}
>
<Text style={{ color: semantics.accentPrimary }}>{action.label}</Text>
</GHPressable>
))}
{closeVisible ? (
<GHPressable
accessibilityLabel={t("a11y/Dismiss notification")}
accessibilityRole="button"
hitSlop={8}
onPress={onDismiss}
style={{ marginLeft: 8 }}
>
<Text style={{ color: semantics.textOnInverse }}>✕</Text>
</GHPressable>
) : null}
</View>
);
};
<WithComponents overrides={{ Notification: CustomNotification }}>
<Chat client={client}>{/* ... */}</Chat>
</WithComponents>;Two accessibility concerns to preserve when overriding:
- Live region: errors should be
assertive(interrupts whatever the screen reader is reading) and other severities should bepolite(queued). - Dismiss label: pull the label from the translation key
a11y/Dismiss notificationrather than hardcoding "Close". The SDK ships translations for 12 locales.
The built-in component translates recognized notification type values before rendering. If you replace it, translate notification.message with t(notification.message) and add any type-specific message handling your app needs.
Level 4: Replace NotificationList
Override NotificationList when you need a different presentation entirely, such as a multi-snackbar stack, a banner at the top of the screen, or a platform-native toast that bypasses React Native rendering. Use useNotificationListController if you only need to swap the visual shell while keeping the SDK's selection, timeout, and target-routing logic:
import Animated, { SlideInDown, SlideOutDown } from "react-native-reanimated";
import {
Chat,
useComponentsContext,
useNotificationListController,
WithComponents,
} from "stream-chat-react-native";
import type { NotificationListProps } from "stream-chat-react-native";
const TopBannerNotificationList = ({
filter,
hostId,
panel,
}: NotificationListProps) => {
const { Notification } = useComponentsContext();
const { dismissNotification, notification } = useNotificationListController({
filter,
hostId,
panel,
});
if (!notification) return null;
return (
<Animated.View
entering={SlideInDown.duration(220)}
exiting={SlideOutDown.duration(180)}
style={{ left: 16, position: "absolute", right: 16, top: 16, zIndex: 20 }}
>
<Notification
notification={notification}
onDismiss={dismissNotification}
showClose={!notification.duration}
/>
</Animated.View>
);
};
<WithComponents overrides={{ NotificationList: TopBannerNotificationList }}>
<Chat client={client}>{/* ... */}</Chat>
</WithComponents>;If you want a stacked snackbar (multiple notifications visible at once), drive the list off useNotifications directly. You're responsible for picking which notifications to show, starting their timeouts, and removing them on dismiss:
import { useEffect } from "react";
import { Pressable, Text, View } from "react-native";
import {
useNotificationApi,
useNotifications,
useNotificationTargetContext,
} from "stream-chat-react-native";
const StackedNotificationList = () => {
const target = useNotificationTargetContext();
const notifications = useNotifications({
filter: (n) => !n.tags?.includes("system"),
requireTarget: true,
target,
});
const { removeNotification, startNotificationTimeout } = useNotificationApi();
useEffect(() => {
notifications.forEach((n) => {
if (!n.duration) return;
startNotificationTimeout(n.id);
});
}, [notifications, startNotificationTimeout]);
return (
<View
style={{
bottom: 16,
left: 16,
position: "absolute",
right: 16,
zIndex: 20,
}}
>
{notifications.slice(-3).map((n) => (
<Pressable
accessibilityLabel={n.message}
accessibilityLiveRegion={
n.severity === "error" ? "assertive" : "polite"
}
accessibilityRole={n.severity === "error" ? "alert" : "summary"}
key={n.id}
onPress={() => removeNotification(n.id)}
style={{
backgroundColor: "#111827",
borderRadius: 12,
marginTop: 8,
padding: 12,
}}
>
<Text style={{ color: "#f9fafb" }}>{n.message}</Text>
</Pressable>
))}
</View>
);
};Animations
NotificationList accepts an enterFrom prop ('bottom' | 'left' | 'right' | 'top', default 'bottom') that picks one of the SDK's bounded-zoom transitions. The same value flows into Notification.entryDirection, so a custom snackbar can mirror the host's direction.
A single notification can also override the entry direction by setting metadata.entryDirection or origin.context.entryDirection when emitted:
addNotification({
message: "Pinned to top",
origin: { emitter: "ChannelListItem", context: { entryDirection: "top" } },
options: { severity: "success" },
});For fully custom motion, replace NotificationList and use any Reanimated transition you like.
Mounting Placement
MessageList, ChannelList, and ThreadList mount a NotificationList for you in the built-in layouts. Channel and Thread provide the target context around their message lists. If you've replaced one of those defaults, mount the snackbar yourself inside the same target context:
<Channel channel={channel}>
<CustomMessageList />
{/* CustomMessageList doesn't render NotificationList, so mount it here */}
<NotificationList panel="channel" />
</Channel>NotificationList also accepts bottomOffset and topOffset props for pushing the snackbar above a floating composer or below a sticky header. The default MessageList already wires bottomOffset to the floating message-input height.
Accessibility
The built-in Notification ships with the right accessibility semantics out of the box:
- Live region:
assertiveforseverity: 'error',politefor everything else. - Role:
alertfor errors,summaryfor other severities. - The dismiss button has a translatable
accessibilityLabelfrom thea11y/Dismiss notificationtranslation key. NotificationListis labelled with thea11y/Notificationskey.- Action buttons have
accessibilityRole='button'.
When overriding Notification or NotificationList, preserve these. Adopt the same live-region rules and use the existing translation keys rather than hardcoding strings. The SDK already translates them across 12 locales.
For animations, respect the user's reduced-motion preference. The SDK's useReducedMotionPreference hook returns true when the OS-level setting is enabled. In your custom snackbar, fall back to instant transitions:
import { useReducedMotionPreference } from "stream-chat-react-native";
const reduceMotion = useReducedMotionPreference();
const duration = reduceMotion ? 0 : 200;Mirror these patterns when shipping a custom snackbar so screen reader and reduced-motion users get the same experience as the rest of the app.