# System notification banner

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

A system notification is about the whole app, not one panel: "Waiting for network…" or "Reconnecting…". `Chat` publishes one while the client can't reach Stream, but renders nothing for it, and no `NotificationList` shows it. This recipe renders system notifications as a bar at the top of the app, with an icon per severity, a slide-in and slide-out animation, and notifications of your own. System notifications and the hook that reads them are explained in [Displaying notifications](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/displaying-notifications/#system-notifications).

## When to use this pattern

- You want a connection banner without writing your own connection handling. `Chat` already reads both connection facts, the device's network and the socket to Stream, and holds a short socket drop back so the banner doesn't flicker. [Connection status](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/connection-status/) explains them.
- You publish app-wide messages, such as an upcoming maintenance, and want them in the same bar.

## Connection status banner

For a connection banner only, keep the lost-connection notification and render its message. Its type is `system:network:connection:lost`, for both messages: "Waiting for network…" when the device reports no network, and "Reconnecting…" when the network is up and only the socket is down. The message is already translated and says which one it is.

Declare the filter at module scope. `useSystemNotifications` builds its subscription from it, so an inline filter resubscribes on every render, as [Selector identity](https://getstream.io/chat/docs/sdk/react/v15/ui/reading-state-in-components/#selector-identity) explains:

```tsx title="src/ConnectionStatusBanner.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 ConnectionStatusBanner = () => {
  const [notification] = useSystemNotifications({ filter: isConnectionLost });
  if (!notification) return null;

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

Mount it inside `Chat`, above the rest of the app, so it reads as a status bar rather than a toast:

```tsx title="src/App.tsx"
import type { PropsWithChildren } from "react";
import { Chat } from "stream-chat-react";
import { ConnectionStatusBanner } from "./ConnectionStatusBanner";

export const App = ({ children }: PropsWithChildren) => (
  <Chat client={client}>
    <div className="app-chat-layout">
      <ConnectionStatusBanner />
      <div className="app-chat-layout__body">{children}</div>
    </div>
  </Chat>
);
```

## Implementation

The full banner shows every system notification, one at a time.

### 1. Banner component

`useSystemNotifications()` returns the system notifications in the order `client.notifications` keeps them, oldest first by default. The banner shows the first. When it's removed, the banner keeps it on screen until the slide-out animation ends. A notification with an action makes the whole bar a button:

```tsx title="src/SystemNotificationBanner.tsx"
import type { ComponentType } from "react";
import { useEffect, useState } from "react";
import type { Notification } from "stream-chat";
import {
  IconCheckmark,
  IconExclamationCircleFill,
  IconExclamationTriangleFill,
  IconInfo,
  IconLoading,
  useSystemNotifications,
} from "stream-chat-react";
import "./SystemNotificationBanner.css";

const iconsBySeverity: Record<string, ComponentType> = {
  error: IconExclamationCircleFill,
  info: IconInfo,
  loading: IconLoading,
  success: IconCheckmark,
  warning: IconExclamationTriangleFill,
};

export const SystemNotificationBanner = () => {
  const [notification] = useSystemNotifications();
  // The last notification shown, kept while the bar slides out.
  const [retained, setRetained] = useState<Notification | undefined>(
    notification,
  );

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

  const shown = notification ?? retained;
  if (!shown) return null;

  const isExiting = !notification;
  const Icon = iconsBySeverity[shown.severity ?? "error"];
  const action = shown.actions?.[0];

  const className = [
    "system-banner",
    shown.severity && `system-banner--${shown.severity}`,
    isExiting && "system-banner--exiting",
    action && "system-banner--interactive",
  ]
    .filter(Boolean)
    .join(" ");

  return (
    <div
      aria-live="polite"
      className={className}
      onAnimationEnd={(event) => {
        if (event.animationName === "system-banner-slide-out") {
          setRetained(undefined);
        }
      }}
      onClick={action?.handler}
      onKeyDown={(event) => {
        if (action && (event.key === "Enter" || event.key === " ")) {
          event.preventDefault();
          action.handler();
        }
      }}
      role={action ? "button" : "status"}
      tabIndex={action ? 0 : undefined}
    >
      {Icon && (
        <span aria-hidden className="system-banner__icon">
          <Icon />
        </span>
      )}
      <span className="system-banner__message">{shown.message}</span>
    </div>
  );
};
```

To show only some system notifications, pass a module-scope `filter`, as in the connection banner. To show several at once, map over the array.

### 2. Styles

The SDK has no styles for this bar. These rules make it an in-flow bar that slides in and out, and spin the icon while the severity is `loading`:

```css title="src/SystemNotificationBanner.css"
.system-banner {
  align-items: center;
  animation: system-banner-slide-in 300ms ease-out both;
  background: var(--str-chat__background-core-surface-default, #ebeef1);
  color: var(--str-chat__text-primary, #414552);
  display: flex;
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 600;
  gap: 8px;
  justify-content: center;
  overflow: hidden;
  padding: 8px 12px;
  width: 100%;
}

.system-banner--exiting {
  animation: system-banner-slide-out 300ms ease-in both;
}

.system-banner--interactive {
  cursor: pointer;
}

.system-banner__icon {
  display: inline-flex;
}

.system-banner--loading .system-banner__icon {
  animation: system-banner-spin 1.5s linear infinite;
}

.system-banner__message {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@keyframes system-banner-slide-in {
  from {
    max-height: 0;
    opacity: 0;
    padding-block: 0;
  }
  to {
    max-height: 4rem;
    opacity: 1;
  }
}

@keyframes system-banner-slide-out {
  from {
    max-height: 4rem;
    opacity: 1;
  }
  to {
    max-height: 0;
    opacity: 0;
    padding-block: 0;
  }
}

@keyframes system-banner-spin {
  to {
    transform: rotate(360deg);
  }
}
```

The `var()` fallbacks apply outside the SDK's `.str-chat` elements, where its design tokens aren't set. To layer these rules with the SDK's own, see [CSS layers](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/theming/#css-layers).

### 3. Mount inside `Chat`

`useSystemNotifications` reads `client.notifications` through `Chat`, so the banner must render inside it. Put it above the layout, in a column:

```tsx title="src/App.tsx"
import type { PropsWithChildren } from "react";
import { Chat } from "stream-chat-react";
import { SystemNotificationBanner } from "./SystemNotificationBanner";
import "./App.css";

export const App = ({ children }: PropsWithChildren) => (
  <Chat client={client}>
    <div className="app-chat-layout">
      <SystemNotificationBanner />
      <div className="app-chat-layout__body">{children}</div>
    </div>
  </Chat>
);
```

```css title="src/App.css"
html,
body,
#root {
  height: 100%;
  margin: 0;
}

.app-chat-layout {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.app-chat-layout__body {
  flex: 1;
  min-height: 0;
}
```

The banner takes the height it needs, and the body, with `ChatView` in it, takes the rest.

### Publishing your own system notifications

`addSystemNotification` from `useNotificationApi()` publishes one. It adds the `system` tag and no panel tags, so the banner shows it and no `NotificationList` does. It takes the same fields as `addNotification`, except `targetPanels`, and returns the notification's id:

```tsx title="src/MaintenanceNotice.tsx"
import { useRef } from "react";
import { useNotificationApi } from "stream-chat-react";

export const MaintenanceNotice = () => {
  const { addSystemNotification, removeNotification } = useNotificationApi();
  const notificationId = useRef<string | null>(null);

  const show = () => {
    notificationId.current = addSystemNotification({
      // No duration, so it stays until removed.
      duration: 0,
      emitter: "MaintenanceNotice",
      message: "Maintenance in 10 minutes",
      severity: "warning",
      type: "app:maintenance:upcoming",
    });
  };

  const hide = () => {
    if (notificationId.current) removeNotification(notificationId.current);
    notificationId.current = null;
  };

  return (
    <>
      <button onClick={show} type="button">
        Announce maintenance
      </button>
      <button onClick={hide} type="button">
        Clear notice
      </button>
    </>
  );
};
```

`tags` you pass are added to the `system` tag. `message` is shown as it is, so translate it before you publish it. [Displaying notifications](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/displaying-notifications/) covers the notification panels, the display filter and translating notifications.

---

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