In-App Notifications

Stream Chat ships an SDK-level notification system that surfaces transient feedback, including errors, confirmations, warnings, and info, inside your app without alerts, modals, or push notifications. The SDK already emits notifications for built-in actions like deleting a message, pinning a channel, sending a poll vote, or uploading an attachment. You can also emit your own notifications from any component, and customize how they're rendered.

The notification UI is the snackbar, a single floating card shown above the message list, channel list, or thread. It animates in, dismisses after a timeout (or stays until the user dismisses it), and supports action buttons.

Best Practices

  • Mount NotificationList once per active target (channel, thread, channel list, thread list) rather than once at the app root, so notifications appear in the context where the action happened.
  • Let the SDK render notifications by default. Override the Notification component or replace NotificationList with WithComponents only when theme tokens are not enough.
  • Use incident instead of hand-rolling the type string. The SDK derives domain:entity:operation:status from the incident, which keeps analytics and dedupe consistent across the codebase.
  • Prefer severity ('error' | 'success' | 'warning' | 'info') over custom severities. Known severities get default durations and live-region semantics; error, warning, and success also get built-in icons.
  • Use duration: 0 sparingly. Notifications without a duration are persistent too. Persistent notifications stay on screen until dismissed and the snackbar shows only one notification at a time, so they block any newer transient notification from appearing.
  • For app-level notifications that should never appear in the snackbar (analytics, background errors, dev warnings), call addSystemNotification and read them via useSystemNotifications.

How It Works

The notification system has three layers:

  1. stream-chat NotificationManager owns the notification store. Every notification is just an object with id, message, origin, severity, type, tags, etc. The manager is exposed as client.notifications and uses a state-store pattern so subscribers re-render when notifications are added, removed, or updated.
  2. SDK hooks: useNotificationApi wraps the manager for emitting, useNotifications reads filtered notifications for a target, and useNotificationTarget infers which panel (channel, thread, channel-list, thread-list) the calling component lives in.
  3. NotificationList is the rendered snackbar host. It picks the highest-priority notification for its target, calls Notification to render it, starts the dismiss timer, and removes the notification when the user swipes or taps the close button.
client.notifications (stream-chat)


useNotificationApi / useNotifications     ← emit / read


NotificationList                          ← picks & renders


Notification + NotificationIcon           ← snackbar UI

When you call addNotification from a component, the SDK tags the notification with the nearest notification target so it appears only in the matching NotificationList. If multiple NotificationList instances are mounted (one per channel, one for the thread, one for the channel list, one for the thread list), each receives only the notifications that belong to it.

Severity Model

Each notification has an optional severity. The built-in Notification component maps each severity to an icon and an accessibility role:

SeverityIconRole / live-region
'error'Exclamation circlealert / assertive
'warning'Exclamation circlesummary / polite
'success'Checkmarksummary / polite
'info'No iconsummary / polite
'loading'Refresh (spinning)summary / polite

Default auto-dismiss durations are configured on the NotificationManager (3 seconds for error, warning, success, and info). loading has a built-in icon but no default duration unless you configure one or pass duration in the notification options.

Panel Targeting

Notifications are routed to UI panels via tags of the form target:<panel> (with optional :<hostId> suffix when targeting a specific channel or thread instance).

The SDK recognizes four built-in panels:

PanelWhere it shows
'channel'Inside an active channel's message list
'thread'Inside the active thread view
'channel-list'On the channel list screen
'thread-list'On the thread list screen

When you call addNotification without passing target or targetPanels, the SDK uses the nearest mounted NotificationTargetProvider. The built-in Channel, Thread, ChannelList, and ThreadList components install that provider for you. You can also pass targetPanels: ['channel-list'] to target a panel explicitly, regardless of where the call originates.

For advanced flows (running a network call after the user navigates away from a channel and routing the resulting notification back to that channel) use runWithNotificationTarget. See the In-App Notifications cookbook.

Lifecycle

A notification goes through three stages:

  1. Created: addNotification(payload) adds it to the store. NotificationList picks it as the active notification if its target matches.
  2. Visible: The snackbar renders. The dismiss timer starts via startNotificationTimeout(id) when the notification has a duration. If the notification has actions, the timer is extended to at least 5 seconds so the user has time to tap.
  3. Dismissed: Times out, the user swipes / taps close, or your code calls removeNotification(id). The active target's NotificationList clears the snackbar.

A notification with duration: 0 or no resolved duration skips step 3's timer and stays until dismissed manually. Persistent notifications take priority over transient ones. If both are present in the same panel, the persistent one wins.

When a NotificationList unmounts, it clears all non-system notifications for its target. This prevents stale notifications from a previous screen leaking into a new one.

Built-in Notifications

The SDK emits notifications automatically for these flows:

FlowType prefixWhere it fires
Message delete / pin / muteapi:message:* / api:user:*useMessageActionHandlers
Channel pin / archive / mute / block userapi:channel:* / api:user:*useChannelActions
Poll create / end / voteapi:poll:* / validation:poll:*messageComposer, usePollState
Reactions fetchapi:message:reactions:fetch:faileduseFetchReactions
Jump to first unreadchannel:jumpToFirstUnread:faileduseMessageListPagination
Audio recordingpermission:audio:* / validation:audio:*AudioRecordingButton, AudioRecorder
Attachment uploadvalidation:attachment:* / api:attachment:upload:failedmessageComposer/attachmentManager
Command validationvalidation:command:disabledtextComposer middleware

These notifications are i18n-aware. Translation keys live in package/src/i18n/<locale>.json, for example, Message deleted, Attachment upload blocked due to {{reason}}, and Failed to create the poll due to {{reason}}. Translate them by overriding the keys in your Streami18n instance.

System Notifications

Some notifications shouldn't show up in the snackbar, such as analytics events, dev-only warnings, or internal sync errors. Emit them with addSystemNotification and they're tagged with the reserved system tag, which excludes them from NotificationList. Read them with useSystemNotifications and forward them to your analytics pipeline, logger, or background queue.

Migration from useInAppNotificationsState

The previous in-app notifications API (useInAppNotificationsState, useClientNotifications, and the recommendation to hand-roll a toaster) is deprecated for snackbar use. The new API renders the snackbar for you and uses the same store as the rest of the SDK.

OldNew
openInAppNotification(notification)addNotification({ message, origin, options }, { incident, targetPanels }) from useNotificationApi
closeInAppNotification(id)removeNotification(id) from useNotificationApi
notifications array from useInAppNotificationsStateuseNotifications()
useClientNotifications()useNotifications() (it now reads the same store the client writes to)
Custom <Toast /> mounted at app root<NotificationList /> mounted by MessageList, ChannelList, and ThreadList in the built-in layouts

The notification object shape is unchanged; it still comes from stream-chat. The only structural change is that created_at is now createdAt (camelCase).

For end-to-end customization examples, including emitting, routing, and custom rendering, see the two cookbook pages: