This is beta documentation for Stream Chat React SDK v14. For the latest stable version, see the latest version (v13) .

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

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)

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

ParameterTypeDescription
messagestringHuman-readable text shown to the user.
emitterstringLogical source (component or feature name) for debugging.
severity'error' | 'warning' | 'info' | 'success'Controls icon and styling. Omit for a plain notification.
durationnumberAuto-dismiss delay in milliseconds. When omitted, falls back to the severity default or stays persistent.
actionsNotificationAction[]Interactive buttons rendered inside the notification (each has a label and handler).
errorErrorUnderlying error stored as options.originalError for debugging.
contextRecord<string, unknown>Arbitrary metadata stored in origin.context.
tagsstring[]Extra tags appended to the notification (useful for custom filtering).
targetPanelsNotificationTargetPanel[]Explicit panels where the notification should appear. When provided, auto-detected panel is ignored.
incidentNotificationIncidentDescriptorStructured descriptor (domain, entity, operation, status). Used to auto-generate type when type is not set.
typestringMachine-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 (errorfailed, successsuccess):

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).

TypeSeverityContextHas TranslatorEmitterSource
api:attachment:upload:failederror{ attachment, failedAttachment }YesAttachmentManagerstream-chat
api:channel:archive:failederror{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:archive:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:leave:failederror{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:leave:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:mute:failederror{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:mute:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:pin:failederror{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:pin:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:unarchive:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:unmute:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:channel:unpin:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:location:create:failederror{ composer }YesMessageComposerstream-chat
api:location:share:failederrorYesShareLocationDialogstream-chat-react
api:message:delete:failederror{ message }YesMessageActionsstream-chat-react
api:message:delete:successsuccess{ message }YesMessageActionsstream-chat-react
api:message:edit:failederrorNoMessageComposerstream-chat-react
api:message:flag:failederror{ message }YesMessageActionsstream-chat-react
api:message:flag:successsuccess{ message }YesMessageActionsstream-chat-react
api:message:markUnread:failederror{ message }YesMessageActionsstream-chat-react
api:message:markUnread:successsuccess{ message }YesMessageActionsstream-chat-react
api:message:pin:failederror{ message }YesMessageActionsstream-chat-react
api:message:pin:successsuccess{ message }YesMessageActionsstream-chat-react
api:message:reactions:fetch:failederrorYesuseFetchReactionsstream-chat-react
api:message:reminder:delete:failederror{ message }NoMessageActionsstream-chat-react
api:message:reminder:delete:successsuccess{ message }NoMessageActionsstream-chat-react
api:message:reminder:set:failederror{ message }NoRemindMeSubmenustream-chat-react
api:message:reminder:set:successsuccess{ message }NoRemindMeSubmenustream-chat-react
api:message:saveForLater:create:failederror{ message }NoMessageActionsstream-chat-react
api:message:saveForLater:create:successsuccess{ message }NoMessageActionsstream-chat-react
api:message:saveForLater:delete:failederror{ message }NoMessageActionsstream-chat-react
api:message:saveForLater:delete:successsuccess{ message }NoMessageActionsstream-chat-react
api:message:send:failederrorNoMessageComposerstream-chat-react
api:message:unpin:failederror{ message }YesMessageActionsstream-chat-react
api:message:unpin:successsuccess{ message }YesMessageActionsstream-chat-react
api:poll:create:failederror{ composer }YesMessageComposerstream-chat
api:poll:end:failederrorYesEndPollAlertstream-chat-react
api:poll:end:successsuccessYesEndPollAlertstream-chat-react
api:reply:search:failederror{ threadReply }YesMessageAlsoSentInChannelIndicatorstream-chat-react
api:user:ban:failederror{ channel }NoChannelListItemActionButtonsstream-chat-react
api:user:ban:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:user:unban:successsuccess{ channel }NoChannelListItemActionButtonsstream-chat-react
api:user:mute:failederror{ message }YesMessageActionsstream-chat-react
api:user:mute:successsuccess{ message }YesMessageActionsstream-chat-react
api:user:unmute:failederror{ message }YesMessageActionsstream-chat-react
api:user:unmute:successsuccess{ message }YesMessageActionsstream-chat-react
audioRecording:cancel:successinfoNoAudioRecorderstream-chat-react
browser:audio:playback:errorerrorYesAudioPlayerstream-chat-react
browser:location:get:failederrorYesShareLocationDialogstream-chat-react
channel:jumpToFirstUnread:failederror{ feature }YesChannelstream-chat-react
system:network:connection:lostloadingNoChatstream-chat-react
validation:attachment:file:missingerror{ attachment }YesAttachmentManagerstream-chat
validation:attachment:id:missingerror{ attachment }YesAttachmentManagerstream-chat
validation:attachment:upload:blockederror{ attachment, blockedAttachment }YesAttachmentManagerstream-chat
validation:attachment:upload:in-progresswarning{ composer }YesMessageComposerstream-chat
validation:poll:castVote:limitinfo{ messageId, optionId }YesPollstream-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:

PanelWhere it appears
channelInside the active channel view
threadInside a thread
channel-listIn the channel list sidebar
thread-listIn 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

PropTypeDefaultDescription
panelNotificationTargetPanelOnly show notifications targeted at this panel.
fallbackPanelNotificationTargetPanel'channel'Panel assumed when a notification has no explicit target.
filter(notification: Notification) => booleanAdditional 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.
classNamestringAdditional 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:

PropTypeDefaultDescription
notificationNotificationThe notification object rendered by this component.
IconComponentType<NotificationIconProps>Default icon by severityCustom icon component. Receives the notification object.
showClosebooleanfalseShow the dismiss button. Automatically true for persistent notifications.
onDismiss() => voidCustom dismiss handler. Falls back to removeNotification(id).
classNamestringAdditional CSS class.

The following props are managed by NotificationList and passed down automatically. You should not set them when overriding Notification:

PropTypeDescription
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

OptionTypeDescription
panelNotificationTargetPanelOnly return notifications for this panel.
fallbackPanelNotificationTargetPanelPanel assumed when a notification has no explicit target. Defaults to 'channel'.
filter(notification: Notification) => booleanAdditional filter applied after panel matching.

useSystemNotifications

Returns notifications tagged for the system banner (the same subset NotificationList excludes). Optional filter narrows further after that.

OptionTypeDescription
filter(notification: Notification) => booleanApplied 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:

ClassApplied to
.str-chat__notificationRoot of each notification
.str-chat__notification-contentContent wrapper (icon + message)
.str-chat__notification-iconIcon container
.str-chat__notification-messageMessage text
.str-chat__notification-actionsAction button row
.str-chat__notification-close-buttonDismiss button

Severity modifier classes (added to the root):

ClassWhen applied
.str-chat__notification--errorseverity is 'error'
.str-chat__notification--warningseverity is 'warning'
.str-chat__notification--infoseverity is 'info'
.str-chat__notification--successseverity is 'success'
.str-chat__notification--loadingseverity is 'loading' (spins the icon)

List container classes:

ClassWhen applied
.str-chat__notification-listAlways on the list wrapper
.str-chat__notification-list--position-topverticalAlignment is 'top'
.str-chat__notification-list--position-bottomverticalAlignment is 'bottom'
.str-chat__notification-list--channelpanel is 'channel'
.str-chat__notification-list--threadpanel is 'thread'
.str-chat__notification-list--channel-listpanel is 'channel-list'
.str-chat__notification-list--thread-listpanel 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

  1. A notification is published with a type (e.g., api:poll:create:failed).
  2. The Notification component calls t('translationBuilderTopic/notification', { notification, value: notification.message }).
  3. The NotificationTranslationTopic looks up a translator registered for that type.
  4. If found, the translator returns a translated string. Otherwise the raw message is passed through t() 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.

TypeDefault 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.