Your code / SDK internals
│
▼
useNotificationApi() ← React hook: addNotification, addSystemNotification, …
│
▼
useNotifications() ← panel toasts (NotificationList)
useSystemNotifications() ← system-banner queue (hidden from NotificationList)
│
▼
NotificationList → Notification ← UI components (and your custom system banner)Notifications
The SDK ships a notification system that lets any part of your application publish short-lived or persistent messages and render them inside the chat UI. Notifications are displayed by the NotificationList component.
Architecture at a Glance
The React layer provides hooks to publish (useNotificationApi) and consume notifications: useNotifications for panel lists (e.g. NotificationList), useSystemNotifications for the global system banner (same items NotificationList filters out), plus UI components (NotificationList, Notification).
Publishing Notifications
Use the useNotificationApi hook inside any component rendered within <Chat>. It returns addNotification, addSystemNotification, removeNotification, and startNotificationTimeout.
addSystemNotification is for the full-width system banner: it always adds the system tag and does not add target:* panel tags (those notifications are hidden from NotificationList and are meant for a custom banner — see the system notification banner cookbook). It returns the notification id. Subscribe with useSystemNotifications. Without ChatProvider you cannot use the hook; use client.notifications.add from the JS client and mirror the same tags and options shape.
import { useNotificationApi } from "stream-chat-react";
function SaveButton() {
const { addNotification } = useNotificationApi();
const handleSave = async () => {
try {
await saveSettings();
addNotification({
emitter: "SaveButton",
message: "Settings saved",
severity: "success",
duration: 3000,
});
} catch (error) {
addNotification({
emitter: "SaveButton",
message: "Could not save settings",
severity: "error",
error: error instanceof Error ? error : undefined,
});
}
};
return <button onClick={handleSave}>Save</button>;
}AddNotificationParams
| Parameter | Type | Description |
|---|---|---|
message | string | Human-readable text shown to the user. |
emitter | string | Logical source (component or feature name) for debugging. |
severity | 'error' | 'warning' | 'info' | 'success' | Controls icon and styling. Omit for a plain notification. |
duration | number | Auto-dismiss delay in milliseconds. When omitted, falls back to the severity default or stays persistent. |
actions | NotificationAction[] | Interactive buttons rendered inside the notification (each has a label and handler). |
error | Error | Underlying error stored as options.originalError for debugging. |
context | Record<string, unknown> | Arbitrary metadata stored in origin.context. |
tags | string[] | Extra tags appended to the notification (useful for custom filtering). |
targetPanels | NotificationTargetPanel[] | Explicit panels where the notification should appear. When provided, auto-detected panel is ignored. |
incident | NotificationIncidentDescriptor | Structured descriptor (domain, entity, operation, status). Used to auto-generate type when type is not set. |
type | string | Machine-readable identifier (domain:entity:operation:status). Drives translation lookup. Auto-generated from incident when omitted. |
Notification with Actions
addNotification({
emitter: "ConnectionMonitor",
message: "Connection lost",
severity: "error",
actions: [
{
label: "Retry",
handler: () => reconnect(),
},
],
});Persistent notifications (no duration) automatically show a close button. Notifications with actions render each action as a small button beside the message.
Notification Types and Incidents
The type field is a free-form string used to identify the kind of notification. It drives translation lookup — the notification translation topic uses it to select the right translator function.
The SDK's built-in notifications follow a domain:entity:operation:status convention (e.g. api:poll:create:failed), but you can use any format you like for your own notifications.
When you provide an incident descriptor, the SDK auto-generates type by joining its fields with colons. If incident.status is omitted, it is inferred from severity (error → failed, success → success):
addNotification({
emitter: "PollCreator",
message: "Poll creation failed",
severity: "error",
incident: {
domain: "api",
entity: "poll",
operation: "create",
},
// type is auto-generated as "api:poll:create:failed"
});You can also set type directly to any string:
addNotification({
emitter: "MyFeature",
message: "Draft saved",
severity: "info",
type: "draft-saved",
});Built-in Notification Types
The following table lists all notification types emitted by the SDK. The Context column shows what data is available in notification.context for custom translators and filters. The Has Translator column indicates whether the type has a dedicated translator registered in the NotificationTranslationTopic (see Customizing Notification Translation). The Source column indicates whether the notification originates from stream-chat (the JS client) or stream-chat-react (the React SDK).
| Type | Severity | Context | Has Translator | Emitter | Source |
|---|---|---|---|---|---|
api:attachment:upload:failed | error | { attachment, failedAttachment } | Yes | AttachmentManager | stream-chat |
api:channel:archive:failed | error | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:archive:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:leave:failed | error | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:leave:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:mute:failed | error | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:mute:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:pin:failed | error | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:pin:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:unarchive:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:unmute:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:channel:unpin:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:location:create:failed | error | { composer } | Yes | MessageComposer | stream-chat |
api:location:share:failed | error | — | Yes | ShareLocationDialog | stream-chat-react |
api:message:delete:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:message:delete:success | success | { message } | Yes | MessageActions | stream-chat-react |
api:message:edit:failed | error | — | No | MessageComposer | stream-chat-react |
api:message:flag:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:message:flag:success | success | { message } | Yes | MessageActions | stream-chat-react |
api:message:markUnread:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:message:markUnread:success | success | { message } | Yes | MessageActions | stream-chat-react |
api:message:pin:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:message:pin:success | success | { message } | Yes | MessageActions | stream-chat-react |
api:message:reactions:fetch:failed | error | — | Yes | useFetchReactions | stream-chat-react |
api:message:reminder:delete:failed | error | { message } | No | MessageActions | stream-chat-react |
api:message:reminder:delete:success | success | { message } | No | MessageActions | stream-chat-react |
api:message:reminder:set:failed | error | { message } | No | RemindMeSubmenu | stream-chat-react |
api:message:reminder:set:success | success | { message } | No | RemindMeSubmenu | stream-chat-react |
api:message:saveForLater:create:failed | error | { message } | No | MessageActions | stream-chat-react |
api:message:saveForLater:create:success | success | { message } | No | MessageActions | stream-chat-react |
api:message:saveForLater:delete:failed | error | { message } | No | MessageActions | stream-chat-react |
api:message:saveForLater:delete:success | success | { message } | No | MessageActions | stream-chat-react |
api:message:send:failed | error | — | No | MessageComposer | stream-chat-react |
api:message:unpin:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:message:unpin:success | success | { message } | Yes | MessageActions | stream-chat-react |
api:poll:create:failed | error | { composer } | Yes | MessageComposer | stream-chat |
api:poll:end:failed | error | — | Yes | EndPollAlert | stream-chat-react |
api:poll:end:success | success | — | Yes | EndPollAlert | stream-chat-react |
api:reply:search:failed | error | { threadReply } | Yes | MessageAlsoSentInChannelIndicator | stream-chat-react |
api:user:ban:failed | error | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:user:ban:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:user:unban:success | success | { channel } | No | ChannelListItemActionButtons | stream-chat-react |
api:user:mute:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:user:mute:success | success | { message } | Yes | MessageActions | stream-chat-react |
api:user:unmute:failed | error | { message } | Yes | MessageActions | stream-chat-react |
api:user:unmute:success | success | { message } | Yes | MessageActions | stream-chat-react |
audioRecording:cancel:success | info | — | No | AudioRecorder | stream-chat-react |
browser:audio:playback:error | error | — | Yes | AudioPlayer | stream-chat-react |
browser:location:get:failed | error | — | Yes | ShareLocationDialog | stream-chat-react |
channel:jumpToFirstUnread:failed | error | { feature } | Yes | Channel | stream-chat-react |
system:network:connection:lost | loading | — | No | Chat | stream-chat-react |
validation:attachment:file:missing | error | { attachment } | Yes | AttachmentManager | stream-chat |
validation:attachment:id:missing | error | { attachment } | Yes | AttachmentManager | stream-chat |
validation:attachment:upload:blocked | error | { attachment, blockedAttachment } | Yes | AttachmentManager | stream-chat |
validation:attachment:upload:in-progress | warning | { composer } | Yes | MessageComposer | stream-chat |
validation:poll:castVote:limit | info | { messageId, optionId } | Yes | Poll | stream-chat |
Types with a translator have their message automatically translated when rendered by the Notification component. Types without a translator fall back to passing the raw message through the i18next t() function, so they are still translatable via standard i18n keys.
Removing Notifications Programmatically
const { removeNotification } = useNotificationApi();
removeNotification(notificationId);Panel Targeting
Every NotificationList can declare which panel it serves. The SDK recognizes four panels:
| Panel | Where it appears |
|---|---|
channel | Inside the active channel view |
thread | Inside a thread |
channel-list | In the channel list sidebar |
thread-list | In the thread list view |
When you call addNotification, the hook automatically tags the notification with the panel inferred from the current React context (e.g., if the emitting component sits inside a <Thread>, the panel is thread). Override this with targetPanels:
addNotification({
emitter: "GlobalAlert",
message: "Scheduled maintenance tonight",
severity: "info",
targetPanels: ["channel", "thread"],
});A NotificationList with panel="channel" only renders notifications tagged for the channel panel. Notifications without an explicit panel fall back to channel (or whatever fallbackPanel is set on the list).
Displaying Notifications
NotificationList
NotificationList is rendered by default inside MessageList, VirtualizedMessageList, ChannelList, and ThreadList. Each instance is scoped to its respective panel. The component shows one notification at a time and animates transitions between them.
import { NotificationList } from "stream-chat-react";
<NotificationList
panel="channel"
enterFrom="bottom"
verticalAlignment="bottom"
/>;NotificationList Props
| Prop | Type | Default | Description |
|---|---|---|---|
panel | NotificationTargetPanel | — | Only show notifications targeted at this panel. |
fallbackPanel | NotificationTargetPanel | 'channel' | Panel assumed when a notification has no explicit target. |
filter | (notification: Notification) => boolean | — | Additional filter applied after panel matching. |
enterFrom | 'bottom' | 'top' | 'left' | 'right' | 'bottom' | Direction the notification slides in from. |
verticalAlignment | 'top' | 'bottom' | 'bottom' | Vertical position of the notification slot within its container. |
className | string | — | Additional CSS class for the list wrapper. |
Filtering Notifications
Use the filter prop to show only specific notifications in a given list:
<NotificationList
panel="channel"
filter={(notification) => notification.origin.emitter === "PollCreator"}
/>Customizing via ComponentContext
Both NotificationList and Notification are registered in ComponentContext and can be replaced using WithComponents. This lets you swap either the list container or the individual notification item (or both).
Replacing NotificationList
Use WithComponents to wrap or replace the default notification list:
import {
Channel,
ChannelHeader,
MessageComposer,
MessageList,
NotificationList,
Thread,
Window,
WithComponents,
type NotificationListProps,
} from "stream-chat-react";
const CustomNotificationList = (props: NotificationListProps) => (
<NotificationList {...props} verticalAlignment="top" enterFrom="top" />
);
const App = () => (
<WithComponents overrides={{ NotificationList: CustomNotificationList }}>
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageComposer />
</Window>
<Thread />
</Channel>
</WithComponents>
);Replacing Notification
To change how each individual notification is rendered while keeping the default list behavior (animations, single-slot queue, panel filtering), override just the Notification component.
The custom component must forward a ref to its root HTMLDivElement. The NotificationList uses the ref to measure the element for entry/exit animations. In ComponentContext, the slot is typed as React.ForwardRefExoticComponent<NotificationProps & React.RefAttributes<HTMLDivElement>>.
import { forwardRef } from "react";
import {
Channel,
Notification,
WithComponents,
type NotificationProps,
} from "stream-chat-react";
const CustomNotification = forwardRef<HTMLDivElement, NotificationProps>(
(props, ref) => (
<Notification
{...props}
ref={ref}
Icon={({ notification }) => (
<span>{notification.severity === "error" ? "⛔" : "ℹ️"}</span>
)}
/>
),
);
const App = () => (
<WithComponents overrides={{ Notification: CustomNotification }}>
<Channel>{/* ... */}</Channel>
</WithComponents>
);The Notification component accepts these props:
| Prop | Type | Default | Description |
|---|---|---|---|
notification | Notification | — | The notification object rendered by this component. |
Icon | ComponentType<NotificationIconProps> | Default icon by severity | Custom icon component. Receives the notification object. |
showClose | boolean | false | Show the dismiss button. Automatically true for persistent notifications. |
onDismiss | () => void | — | Custom dismiss handler. Falls back to removeNotification(id). |
className | string | — | Additional CSS class. |
The following props are managed by NotificationList and passed down automatically. You should not set them when overriding Notification:
| Prop | Type | Description |
|---|---|---|
entryDirection | 'bottom' | 'top' | 'left' | 'right' | Direction from which the notification enters. |
transitionState | 'enter' | 'exit' | Current animation state. |
Using useNotifications Directly
For fully custom rendering, use the useNotifications hook to subscribe to notifications directly:
import { useNotifications, useNotificationApi } from "stream-chat-react";
function CustomNotificationUI() {
const notifications = useNotifications({ panel: "channel" });
const { removeNotification } = useNotificationApi();
return (
<ul>
{notifications.map((n) => (
<li key={n.id} className={`severity-${n.severity}`}>
{n.message}
<button onClick={() => removeNotification(n.id)}>Dismiss</button>
</li>
))}
</ul>
);
}useNotifications Options
| Option | Type | Description |
|---|---|---|
panel | NotificationTargetPanel | Only return notifications for this panel. |
fallbackPanel | NotificationTargetPanel | Panel assumed when a notification has no explicit target. Defaults to 'channel'. |
filter | (notification: Notification) => boolean | Additional filter applied after panel matching. |
useSystemNotifications
Returns notifications tagged for the system banner (the same subset NotificationList excludes). Optional filter narrows further after that.
| Option | Type | Description |
|---|---|---|
filter | (notification: Notification) => boolean | Applied after the built-in system-tag filter. |
See the system notification banner cookbook for a full UI example.
Other Notification-Like Components
The message list renders a few floating UI elements alongside NotificationList that serve a different purpose. These are not part of the notification system but are visually similar:
UnreadMessagesNotification— appears when the user is scrolled up and there are unread messages below.NewMessageNotification— shown when new messages arrive while the user is scrolled away from the bottom.ScrollToLatestMessageButton— a button to jump back to the most recent messages.
Each of these is a separate component override in ComponentContext and can be replaced independently via WithComponents.
Styling Notifications
CSS Variables
The notification system exposes CSS custom properties that you can override in your theme:
/* Notification container */
--str-chat__notification-background: var(--background-core-inverse);
--str-chat__notification-color: var(--text-inverse);
--str-chat__notification-border-radius: var(--radius-3xl);
/* Notification list positioning */
--str-chat__notification-list-inset: 16px;
--str-chat__notification-list-gap: 8px;CSS Class Names
Use these class names to target specific parts of the notification UI in your stylesheets — for example, to resize the icon, change the message font, style action buttons differently, or apply distinct looks per severity level.
Notification element classes:
| Class | Applied to |
|---|---|
.str-chat__notification | Root of each notification |
.str-chat__notification-content | Content wrapper (icon + message) |
.str-chat__notification-icon | Icon container |
.str-chat__notification-message | Message text |
.str-chat__notification-actions | Action button row |
.str-chat__notification-close-button | Dismiss button |
Severity modifier classes (added to the root):
| Class | When applied |
|---|---|
.str-chat__notification--error | severity is 'error' |
.str-chat__notification--warning | severity is 'warning' |
.str-chat__notification--info | severity is 'info' |
.str-chat__notification--success | severity is 'success' |
.str-chat__notification--loading | severity is 'loading' (spins the icon) |
List container classes:
| Class | When applied |
|---|---|
.str-chat__notification-list | Always on the list wrapper |
.str-chat__notification-list--position-top | verticalAlignment is 'top' |
.str-chat__notification-list--position-bottom | verticalAlignment is 'bottom' |
.str-chat__notification-list--channel | panel is 'channel' |
.str-chat__notification-list--thread | panel is 'thread' |
.str-chat__notification-list--channel-list | panel is 'channel-list' |
.str-chat__notification-list--thread-list | panel is 'thread-list' |
The panel class lets you scope styles to a specific notification list. For example, to make channel-list notifications appear narrower:
.str-chat__notification-list--channel-list .str-chat__notification {
max-width: 250px;
}Severity-Specific Styling Example
.str-chat__notification--error {
--str-chat__notification-background: #dc2626;
--str-chat__notification-color: #fff;
}
.str-chat__notification--success {
--str-chat__notification-background: #16a34a;
--str-chat__notification-color: #fff;
}Entry and Exit Animations
Notifications animate in and out based on the enterFrom direction. Entry uses a slide + fade (760 ms, ease-out), exit reverses the direction (340 ms). You can customize timing by overriding the animation declarations on .str-chat__notification--is-entering and .str-chat__notification--is-exiting:
/* Slow down the entry animation */
.str-chat__notification--is-entering {
animation-duration: 1200ms;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
/* Speed up the exit animation */
.str-chat__notification--is-exiting {
animation-duration: 200ms;
animation-timing-function: ease-in;
}Customizing Notification Translation
The Notification component translates its message through the notification translation topic. This lets you replace raw messages with locale-aware text without changing the code that publishes them.
For most cases, the simplest approach is to translate the message at the call site using the t function and pass the translated string to addNotification:
addNotification({
emitter: "MyFeature",
message: t("Draft saved successfully"),
severity: "success",
duration: 3000,
});Custom translators are useful when the conversion from a notification object to a display string involves dynamic logic — for example, when the translated text depends on notification.metadata fields like a reason or entity name, or when you need to produce different translations for the same notification type depending on context. If your notification text is a simple static string, prefer the t() approach above.
How It Works
- A notification is published with a
type(e.g.,api:poll:create:failed). - The
Notificationcomponent callst('translationBuilderTopic/notification', { notification, value: notification.message }). - The
NotificationTranslationTopiclooks up a translator registered for thattype. - If found, the translator returns a translated string. Otherwise the raw
messageis passed throught()as a natural i18next key.
Built-in Translators
The table below lists translators that involve custom logic — typically dynamic interpolation based on notification.metadata fields. These translators exist because the corresponding notifications are published from the low-level stream-chat client, which does not have access to the React translation service. The translators bridge that gap by converting the raw notification into a properly translated string at render time.
| Type | Default Translation |
|---|---|
api:attachment:upload:failed | "Error uploading attachment" (or "Attachment upload failed due to {{reason}}") |
api:location:create:failed | "Failed to share location" |
api:location:share:failed | "Failed to share location" |
api:poll:create:failed | "Failed to create the poll" (or "Failed to create the poll due to {{reason}}") |
api:poll:end:failed | "Failed to end the poll" (or "Failed to end the poll due to {{reason}}") |
api:poll:end:success | "Poll ended" |
api:reply:search:failed | "Thread has not been found" |
browser:audio:playback:error | "Error reproducing the recording" |
browser:location:get:failed | "Failed to retrieve location" |
channel:jumpToFirstUnread:failed | "Failed to jump to the first unread message" |
validation:attachment:file:missing | "File is required for upload attachment" |
validation:attachment:id:missing | "Local upload attachment missing local id" |
validation:attachment:upload:blocked | "Attachment upload blocked due to {{reason}}" |
validation:attachment:upload:in-progress | "Wait until all attachments have uploaded" |
validation:poll:castVote:limit | "Reached the vote limit. Remove an existing vote first." |
Registering Custom Translators
Each translator receives { key, value, t, options } where options.notification is the full notification object. Return a translated string or null to fall through to the next translator.
The example below shows how to override translators for message action notifications. These notifications include context.message (set by MessageActions), which lets you interpolate message-specific data into the translated string:
import { Streami18n } from "stream-chat-react";
import type {
NotificationTranslatorOptions,
Translator,
} from "stream-chat-react";
const customTranslators: Record<
string,
Translator<NotificationTranslatorOptions>
> = {
"api:message:pin:success": ({ options: { notification }, t }) => {
const message = notification?.context?.message;
return t("Pinned message {{id}}", { id: message?.id });
},
"api:message:pin:failed": ({ options: { notification }, t }) => {
const message = notification?.context?.message;
return t("Failed to pin message {{id}}", { id: message?.id });
},
"api:user:mute:success": ({ options: { notification }, t }) => {
const message = notification?.context?.message;
const actor = message?.user?.name || message?.user?.id;
return t("Muted {{actor}} from message {{id}}", {
actor,
id: message?.id,
});
},
"api:user:mute:failed": ({ options: { notification }, t }) => {
const message = notification?.context?.message;
const actor = message?.user?.name || message?.user?.id;
const reason = notification?.error?.message;
if (reason)
return t("Failed to mute {{actor}}: {{reason}}", { actor, reason });
return t("Failed to mute {{actor}}", { actor });
},
};
const i18nInstance = new Streami18n();
i18nInstance.translationBuilder.registerTranslators(
"notification",
customTranslators,
);The example above covers only a few types. Consult the Built-in Notification Types table for the full list of types you can register custom translators for.
The wildcard translator (*) is the catch-all: it tries to match by notification.type in the built-in translator registry. If you register a translator keyed by the exact type string, it takes precedence over the wildcard.
For more details on the TranslationBuilder system (creating custom topics, the i18next post-processor integration, etc.), see the Translation Builder section of the Translations guide.