# System notification banner

Panel `NotificationList` components **hide** notifications that carry the `system` tag, so those items never appear as inline toasts. The `<Chat>` component still **publishes** a persistent notification when the WebSocket connection drops (type: `system:network:connection:lost`). To show that, or any other `system`-tagged notification, render a small subscriber component **inside** `<Chat>` next to your layout.

Use **`useSystemNotifications`** from `stream-chat-react` to subscribe to the same subset `NotificationList` hides from toasts (system-banner notifications). This is the recommended pattern for connection-status banners and other app-wide system messages.

## When to use this pattern

- You want a global connection banner without duplicating `connection.changed` listeners.
- You publish with `addSystemNotification` and want them in the same banner.

## Connection Status Banner

If you only want a connection-status UI, filter the system notifications down to the built-in lost-connection event and render a small banner for that one type.

The default `<Chat>` component already publishes `system:network:connection:lost` through `useReportLostConnectionSystemNotification()`, so most apps only need to mount the banner UI.

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

export function ConnectionStatusBanner() {
  const notifications = useSystemNotifications({
    filter: (notification) =>
      notification.type === "system:network:connection:lost",
  });

  const notification = notifications[0];
  if (!notification) return null;

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

Mount it near the top of your app chat shell so it behaves like a status bar, not like a toast:

```tsx
<Chat client={client}>
  <div className="app-chat-layout">
    <ConnectionStatusBanner />
    <div className="app-chat-layout__body">{/* main chat UI */}</div>
  </div>
</Chat>
```

## Implementation

### 1. Banner component

Call **`useSystemNotifications()`** to get every active system-banner notification as an array (same ordering as the client store). The sample below is a **single-slot** UI: it reads **`notifications[0]`** only, so if more than one system notification exists at once, the rest are ignored until that one is removed. To show several at a time, map over `notifications` or implement your own queue / stacking.

```tsx
import clsx from "clsx";
import type { Notification, NotificationSeverity } from "stream-chat";
import { type ComponentType, useEffect, useState } from "react";
import {
  IconCheckmark,
  IconExclamationCircleFill,
  IconExclamationTriangleFill,
  IconLoading,
  createIcon,
  useSystemNotifications,
} from "stream-chat-react";

export const IconInfoCircle = createIcon(
  "IconInfoCircle",
  <path
    d="M4.42891 16.875L12.9953 8.30781C13.0534 8.2497 13.1223 8.2036 13.1982 8.17215C13.274 8.1407 13.3554 8.12451 13.4375 8.12451C13.5196 8.12451 13.601 8.1407 13.6768 8.17215C13.7527 8.2036 13.8216 8.2497 13.8797 8.30781L16.875 11.3039M3.75 3.125H16.25C16.5952 3.125 16.875 3.40482 16.875 3.75V16.25C16.875 16.5952 16.5952 16.875 16.25 16.875H3.75C3.40482 16.875 3.125 16.5952 3.125 16.25V3.75C3.125 3.40482 3.40482 3.125 3.75 3.125ZM8.75 7.5C8.75 8.19036 8.19036 8.75 7.5 8.75C6.80964 8.75 6.25 8.19036 6.25 7.5C6.25 6.80964 6.80964 6.25 7.5 6.25C8.19036 6.25 8.75 6.80964 8.75 7.5Z"
    fill="none"
    stroke="currentColor"
    strokeLinecap="round"
    strokeLinejoin="round"
    strokeWidth="1.5"
  />,
);

const IconsBySeverity: Record<NotificationSeverity, ComponentType | null> = {
  error: IconExclamationCircleFill,
  info: IconInfoCircle,
  loading: IconLoading,
  success: IconCheckmark,
  warning: IconExclamationTriangleFill,
};

export function SystemNotification() {
  const notifications = useSystemNotifications();
  const notification = notifications[0];
  const [retained, setRetained] = useState<Notification | undefined>(
    notification,
  );

  useEffect(() => {
    if (notification) setRetained(notification);
  }, [notification]);

  const isExiting = !notification && !!retained;
  const rendered = notification ?? retained;
  if (!rendered) return null;

  const Icon = rendered.severity
    ? (IconsBySeverity[rendered.severity] ?? null)
    : IconExclamationCircleFill;
  const action = rendered.actions?.[0];

  return (
    <div
      aria-live="polite"
      className={clsx("str-chat__system-notification", {
        "str-chat__system-notification--exiting": isExiting,
        "str-chat__system-notification--interactive": action,
        [`str-chat__system-notification--${rendered.severity}`]:
          rendered.severity,
      })}
      onAnimationEnd={(e) => {
        if (e.animationName === "str-chat__system-notification-slide-out") {
          setRetained(undefined);
        }
      }}
      onClick={action?.handler}
      onKeyDown={
        action
          ? (event) => {
              if (event.key === "Enter" || event.key === " ") {
                event.preventDefault();
                action.handler();
              }
            }
          : undefined
      }
      role={action ? "button" : "status"}
      tabIndex={action ? 0 : undefined}
    >
      {Icon && (
        <span aria-hidden className="str-chat__system-notification-icon">
          <Icon />
        </span>
      )}
      <span className="str-chat__system-notification-message">
        {rendered.message}
      </span>
    </div>
  );
}
```

Pass **`filter`** to `useSystemNotifications({ filter })` to narrow further (e.g. only `system:network:connection:lost`).

### 2. Styles

The banner uses BEM classes under `str-chat__system-notification*`. Ship matching rules in your own stylesheet (in-flow bar, slide-in/out on mount and unmount, spinner on the icon when severity is `loading`). Import that sheet inside an appropriate [CSS cascade layer](/chat/docs/sdk/react/theming/themingv2/) when you need to override base styles—for example `@import url('./SystemNotification.scss') layer(stream-app-overrides);`.

### 3. Mount inside `Chat`

`useSystemNotifications` requires the chat client context from `<Chat>` (same as `useNotifications`).

```tsx
<Chat client={client}>
  <div className="app-chat-layout">
    <SystemNotification />
    <div className="app-chat-layout__body">{/* main chat UI */}</div>
  </div>
</Chat>
```

Pair this with a column flex shell (for example `#root { height: 100vh; display: flex; flex-direction: column; min-height: 0 }`) and layout rules so `.app-chat-layout` and `.app-chat-layout__body` use `flex: 1`, `min-height: 0`, and the `ChatView` root grows within the body beneath the banner.

### Publishing your own system notifications

Prefer `useNotificationApi().addSystemNotification`: it applies the `system` tag, omits `target:*` panel tags (so the item stays global), and accepts the same fields as `addNotification` except `targetPanels`.

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

function MaintenanceBannerTrigger() {
  const { addSystemNotification } = useNotificationApi();

  const notify = () => {
    addSystemNotification({
      emitter: "MaintenanceScheduler",
      message: "Maintenance in 10 minutes",
      duration: 0,
      severity: "warning",
      type: "app:maintenance:upcoming",
    });
  };

  return (
    <button type="button" onClick={notify}>
      Schedule notice
    </button>
  );
}
```

Optional `tags` are merged with the built-in `system` tag (deduplicated).

See the [notifications guide](/chat/docs/sdk/react/guides/notifications/) for the full notification model, `NotificationList`, and translation hooks.


---

This page was last updated at 2026-04-17T17:33:51.684Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/guides/customization/system-notification-banner/](https://getstream.io/chat/docs/sdk/react/guides/customization/system-notification-banner/).