Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

System notification banner

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.

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 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 explains:

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:

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:

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:

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.

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:

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>
);
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:

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 covers the notification panels, the display filter and translating notifications.