# Displaying notifications

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

Notifications live in `client.notifications`, in the state layer: who publishes them and what they contain is on [Publishing notifications](https://getstream.io/chat/docs/sdk/react/v15/state/publishing-notifications/). This page is about showing them. `NotificationList` components show them on screen, one in each part of the layout, each showing one notification at a time.

>
> **Info:** **Example**
>
> A user pins a message in a thread:
>
> 1. The pin action publishes "Message pinned". Because the action sits inside a `Thread`, the notification is tagged for the `thread` panel.
> 2. The thread's message list renders a `NotificationList` for the `thread` panel. It shows the notification at the bottom of the thread.
> 3. The channel's message list renders a `NotificationList` for the `channel` panel, and ignores it.
> 4. A few seconds after it appears, the notification dismisses itself.
>

## Notification lists and panels

A panel is a part of the screen with its own notification list. The SDK renders one list per panel:

| Panel          | Rendered by                                                   |
| -------------- | ------------------------------------------------------------- |
| `channel`      | `MessageList` and `VirtualizedMessageList` inside a `Channel` |
| `thread`       | `MessageList` and `VirtualizedMessageList` inside a `Thread`  |
| `channel-list` | `ChannelNavigation`, below its channel lists                  |
| `thread-list`  | `ThreadList`                                                  |
| `modal`        | each open modal (`GlobalModal`)                               |

A list shows one notification at a time and queues the rest:

- **A persistent notification goes first.** One without a duration, usually one that carries an action such as "Retry", jumps the queue and stays until it's dismissed or a newer persistent one arrives.
- **A repeat replaces its original.** When a notification of the same `type` arrives while one with a duration is showing, the new one takes its place at once.
- **Otherwise the oldest goes next.** A notification stays on screen for at least `minDisplayMs` (1000 ms by default) before a queued one replaces it.
- **The countdown starts when it's seen.** A notification's auto-dismiss timer starts when it becomes visible in a list, so one waiting in a queue doesn't expire unseen.

`pickNext` changes which notification goes next. `createDefaultPickNext(pickNewest)` keeps the rules above but shows the newest queued notification instead of the oldest.

`NotificationList` takes these props:

| Prop                | Default                             | What it does                                                                       |
| ------------------- | ----------------------------------- | ---------------------------------------------------------------------------------- |
| `panel`             |                                     | the panel the list serves                                                          |
| `fallbackPanel`     | `'channel'`                         | the panel an untagged notification belongs to                                      |
| `filter`            |                                     | `(notification) => boolean`, run after routing and the display filter              |
| `enterFrom`         | `'bottom'`                          | the side a notification slides in from: `'bottom'`, `'top'`, `'left'` or `'right'` |
| `verticalAlignment` | `'bottom'`                          | where the notification sits in the list: `'top'` or `'bottom'`                     |
| `minDisplayMs`      | `1000`                              | how long a notification stays before a queued one can replace it                   |
| `pickNext`          | `createDefaultPickNext(pickOldest)` | `(notifications, displayed) => Notification \| null`: what to show next            |
| `className`         |                                     | a class for the list's element                                                     |

### Replacing the list or a notification

Both are keys in `ComponentContext`. `NotificationList` replaces every list, the modal's included, and `Notification` replaces how each notification renders:

```tsx
import { forwardRef } from "react";
import type { PropsWithChildren } from "react";
import {
  createDefaultPickNext,
  Notification,
  NotificationList,
  pickNewest,
  WithComponents,
} from "stream-chat-react";
import type {
  NotificationListProps,
  NotificationProps,
} from "stream-chat-react";

const pickNewestFirst = createDefaultPickNext(pickNewest);

// Every list slides in from the top and shows the newest queued notification first.
const TopNotificationList = (props: NotificationListProps) => (
  <NotificationList
    {...props}
    enterFrom="top"
    pickNext={pickNewestFirst}
    verticalAlignment="top"
  />
);

// The list starts a notification's countdown once its element is visible,
// so the component forwards its ref.
const BorderedNotification = forwardRef<HTMLDivElement, NotificationProps>(
  (props, ref) => (
    <Notification {...props} className="bordered-notification" ref={ref} />
  ),
);

const notificationOverrides = {
  Notification: BorderedNotification,
  NotificationList: TopNotificationList,
};

export const WithTopNotifications = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={notificationOverrides}>{children}</WithComponents>
);
```

`Notification` takes `notification`, `className`, an `Icon` that receives the notification, `showClose`, and `onDismiss`, which defaults to removing the notification. `NotificationList` passes `showClose` for persistent notifications, and sets the animation props `entryDirection` and `transitionState` itself.

To render notifications your own way, read them with `useNotifications({ panel })`. It returns system notifications too: drop them with `hasSystemNotificationTag`. A notification you show this way only expires if you start its countdown, with `ensureNotificationTimeout(id)` from `useNotificationApi()`.

### Styling

Three CSS variables style a notification: `--str-chat__notification-background`, `--str-chat__notification-color` and `--str-chat__notification-border-radius`. Each notification has the class `str-chat__notification--<severity>`, for example `str-chat__notification--error`, and each list the class `str-chat__notification-list--<panel>`:

```css
.str-chat__notification--error {
  --str-chat__notification-background: #b91c1c;
  --str-chat__notification-color: #fff;
}

.str-chat__notification-list--channel-list .str-chat__notification {
  max-width: 250px;
}
```

## Routing notifications

A notification reaches a list through tags of the form `target:<panel>`:

- **A list shows a notification tagged for its panel.** A notification can be tagged for several panels.
- **An untagged notification goes to the `channel` panel.** A list's `fallbackPanel` prop changes that for the list.
- **A notification from `stream-chat` carries no tags.** When a composer published it, the list goes by the composer: a thread's composer means `thread`, a channel's means `channel`. Any other goes to the `channel` panel.

When a component publishes with `useNotificationApi()`, `addNotification` adds the tag for the panel the component sits in: `thread` inside a `Thread`, `channel` inside a `Channel`, `thread-list` in the threads view, `channel-list` inside `ChannelLists`. `targetPanels` replaces that guess:

```tsx
import { useNotificationApi } from "stream-chat-react";

export const CopyInviteLinkButton = ({ link }: { link: string }) => {
  const { addNotification } = useNotificationApi();

  return (
    <button
      onClick={async () => {
        await navigator.clipboard.writeText(link);
        addNotification({
          emitter: "CopyInviteLinkButton",
          message: "Invite link copied",
          severity: "success",
          // Shown in the channel list, wherever the button is.
          targetPanels: ["channel-list"],
        });
      }}
      type="button"
    >
      Copy invite link
    </button>
  );
};
```

While a modal is open, `addNotification` also tags the notification for the `modal` panel, so it appears in the modal instead of behind it.

A list's `filter` prop narrows what it shows after routing. Declare the filter at module scope: it becomes part of the list's subscription, and a new function on every render resubscribes it.

## Display filter

`Chat` takes a `notificationDisplayFilter`. Every `NotificationList` calls it after routing has matched a notification to the list, and before the list's own `filter`. It receives the notification and the list's `panel`, and returns whether the list may show it.

The default filter handles modals:

- While a modal is open, notifications show only in the modal's list. Notifications for other panels are held back until the modal closes.
- A notification tagged for a modal that has already closed shows in its other panel instead, so a confirmation sent as a dialog closes isn't lost.

A filter you pass replaces the default, modal handling included:

```tsx
import { Chat } from "stream-chat-react";
import type { NotificationDisplayFilter } from "stream-chat-react";
import type { PropsWithChildren } from "react";

// Keep success confirmations out of the channel list. Every other notification shows.
const notificationDisplayFilter: NotificationDisplayFilter = ({
  notification,
  panel,
}) => !(panel === "channel-list" && notification.severity === "success");

export const App = ({ children }: PropsWithChildren) => (
  <Chat client={client} notificationDisplayFilter={notificationDisplayFilter}>
    {children}
  </Chat>
);
```

`NotificationConfigurationProvider` sets a display filter for part of the tree: inside `Chat`, its `displayFilter` replaces the one from above for every list below it. `useNotifications` applies the display filter only when you pass `applyDisplayFilter: true`.

## Translating notifications

The `Notification` component translates each notification as it renders it, through the `notification` translation topic:

1. A translator registered for the notification's `type` decides the text.
2. Otherwise the SDK's own translators do. There is one for every type `stream-chat` publishes, and a few for types the React components publish. They render keys in the `notification.*` namespace.
3. Otherwise the list renders `notification.message` as it is.

The React components mostly translate their notifications before publishing them: "Message pinned" is `t('common.messagePinned.label')`. So there are three ways to change the text:

- **A notification from a React component:** override the translation key it was published with. When a message action fails with an error message of its own, the notification shows that message. An audio playback error always shows `notification.audioPlaybackError`.
- **A notification from `stream-chat`:** override its `notification.*` key.
- **Text that depends on the notification:** register a translator for its type.

```ts
import { asDynamicKey, Streami18n } from "stream-chat-react";
import type {
  NotificationTranslatorOptions,
  Translator,
} from "stream-chat-react";

const i18nInstance = new Streami18n({
  translationsForLanguage: {
    // Published by stream-chat: an upload that failed.
    "notification.attachmentUploadFailed": "That file could not be uploaded",
    // Published by the pin action, translated at the call site.
    "common.messagePinned.label": "Pinned",
  },
});

// Names the author of the pinned message.
const pinTranslators: Record<
  string,
  Translator<NotificationTranslatorOptions>
> = {
  "api:message:pin:success": ({ options: { notification }, t }) => {
    const message = notification?.origin.context?.message as
      { user?: { name?: string } } | undefined;
    return t(asDynamicKey("myApp.notification.messagePinned"), {
      author: message?.user?.name ?? "someone",
      defaultValue: "Pinned a message from {{ author }}",
    });
  },
};

i18nInstance.translationBuilder.registerTranslators(
  "notification",
  pinTranslators,
);
```

Pass `i18nInstance` to `Chat`. A translator receives the notification as `options.notification`, and returns a string, or `null` to show `notification.message` as it is. Keys of your own go through `asDynamicKey`: see [Localization](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/#translation-builder) for the translation builder and dictionaries.

## System notifications

A system notification is about the whole app, not a panel: "Waiting for network…" or "Reconnecting…". It carries the `system` tag and no `target:` tags, and no `NotificationList` shows it.

`Chat` publishes one while the client can't reach Stream:

- "Waiting for network…" as soon as the device reports it has no network;
- "Reconnecting…" when the socket has been down for `client.wsConnection.config.offlineNotificationDisplayDelayMs`. A drop shorter than that shows nothing.

Both have the type `system:network:connection:lost` and the severity `loading`, stay until the connection is back, and then disappear. `Chat` renders no banner for them. Render your own with `useSystemNotifications`, anywhere inside `Chat`:

```tsx
import type { Notification } from "stream-chat";
import { useSystemNotifications } from "stream-chat-react";

const isConnectionLost = (notification: Notification) =>
  notification.type === "system:network:connection:lost";

export const ConnectionBanner = () => {
  const [lostConnection] = useSystemNotifications({ filter: isConnectionLost });
  if (!lostConnection) return null;

  return (
    <div className="connection-banner" role="status">
      {lostConnection.message}
    </div>
  );
};
```

The message is already translated: `Chat` publishes it with `t()`. To publish a system notification of your own, call `addSystemNotification` from `useNotificationApi()`: it returns the id to remove it with. The [System notification banner](https://getstream.io/chat/docs/sdk/react/v15/recipes/app/system-notification-banner/) recipe builds a complete banner, and [Connection status](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/connection-status/) explains the connection states behind it.

## What the React components publish

These components publish notifications. Each one sets `emitter` to the name in brackets:

| Component                                                                                      | Types                                                                                                                                                                                                                                                                 |
| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Message actions (`MessageActions`)                                                             | `api:message:pin:*`, `api:message:unpin:*`, `api:message:markUnread:*`, `api:message:flag:*`, `api:message:delete:*`, `api:user:mute:*`, `api:user:unmute:*`                                                                                                          |
| Reminders and save for later (`MessageActions`)                                                | `api:message:reminder:set:*`, `api:message:reminder:delete:*`, `api:message:saveForLater:create:*`, `api:message:saveForLater:delete:*`                                                                                                                               |
| Channel-list row actions (`ChannelListItemActionButtons`)                                      | `api:channel:archive:*`, `api:channel:unarchive:success`, `api:channel:pin:*`, `api:channel:unpin:success`, `api:channel:mute:*`, `api:channel:unmute:success`, `api:channel:leave:*`, `api:user:ban:*`, `api:user:unban:success`                                     |
| Channel details actions (`ChannelManagementView`, `ChannelMemberDetail`, `ChannelMembersView`) | `api:channel:mute:*`, `api:channel:unmute:*`, `api:channel:leave:*`, `api:channel:delete:*`, `api:channel:addMembers:*`, `api:channel:remove-members:*`, `api:channel:watch:failed`, `api:user:mute:*`, `api:user:unmute:*`, `api:user:block:*`, `api:user:unblock:*` |
| Editing a channel in its details (`ChannelManagementEditBody`)                                 | `channel:channel:update:success`, `api:channel:update:failed`                                                                                                                                                                                                         |
| Polls (`PollCreationDialog`, `EndPollAlert`)                                                   | `api:poll:create:success`, `api:poll:end:*`                                                                                                                                                                                                                           |
| Location (`ShareLocationDialog`)                                                               | `api:location:share:failed`, `browser:location:get:failed`                                                                                                                                                                                                            |
| Audio recording (`AudioRecorder`)                                                              | `audioRecording:cancel:success`                                                                                                                                                                                                                                       |
| Audio playback (`AudioPlayer`)                                                                 | `browser:audio:playback:error`                                                                                                                                                                                                                                        |
| Reactions (`Reactions`)                                                                        | `api:message:reactions:fetch:failed`                                                                                                                                                                                                                                  |
| Search results (`ChannelSearchResultItem`, `MessageSearchResultItem`, `UserSearchResultItem`)  | `api:channel:watch:failed`, when the opened channel fails to load                                                                                                                                                                                                     |
| Viewing a reply in its channel (`useMessageAlsoSentInChannelNavigation`)                       | `api:message:search:not-found`                                                                                                                                                                                                                                        |
| Channel list (`ChannelList`)                                                                   | no type: the failed query's error message, untranslated                                                                                                                                                                                                               |
| Lost connection (`Chat`)                                                                       | `system:network:connection:lost`, a system notification                                                                                                                                                                                                               |

`*` stands for `success` and `failed`. The pin, unpin, delete, mute and unmute message actions are the exception: their handlers catch the request error, so only the `success` type is published, and a failed request still shows the success notification.

Most messages are translated with `t()` before they're published. Three kinds aren't:

- A failed message action whose error carries a message shows that message.
- A failed reminder or save for later, without such a message, shows English text.
- The channel list shows the failed query's error message.

The search results, the reply view and the channel list publish without a panel tag, so their notifications show in the `channel` panel.

Uploads, sending, message-list jumps and poll votes are published by `stream-chat`: see [Publishing notifications](https://getstream.io/chat/docs/sdk/react/v15/state/publishing-notifications/).

Used in: [Notifications](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/notifications/) and [Connection status](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/connection-status/).

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/displaying-notifications/](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/displaying-notifications/).