Skip to content

Snackbar

The SDK renders in-app notifications as a snackbar: a single floating card mounted by NotificationList and rendered by Notification. This cookbook walks through customizing how that snackbar looks and behaves, from the lightest touch (theme tokens) to a full replacement (your own snackbar component on top of the SDK's notification store).

If you only need to know how to emit notifications, see the In-App Notifications cookbook instead.

Best Practices

  • Prefer theme tokens. The vast majority of brand-matching tweaks (colors, spacing, typography) need no overrides.
  • Mount NotificationList once per active target. MessageList, ChannelList, and ThreadList already do this in the built-in layouts. Only mount your own when you've replaced those default child components.
  • When overriding Notification, provide it through WithComponents and preserve the SDK's accessibility props: live-region for errors (assertive) vs other severities (polite), accessibilityRole='alert' for errors and 'summary' otherwise, and a translatable accessibilityLabel on the close button.
  • Don't mix custom and built-in snackbars at the same time. Pick one extension level and stick with it per app.
  • If you need entirely different motion (e.g. top-of-screen banners instead of bottom snackbar), replace NotificationList rather than fighting the built-in animations.

Level 1: Theme Tokens

The fastest customization is restyling. The theme exposes per-component slots for the snackbar:

import { Chat, OverlayProvider } from "stream-chat-react-native";

const theme = {
  notification: {
    container: {
      backgroundColor: "#1f2937",
      borderRadius: 16,
      paddingHorizontal: 16,
      paddingVertical: 12,
    },
    message: {
      color: "#f9fafb",
      fontSize: 14,
    },
    actionButton: {
      backgroundColor: "#374151",
    },
    closeButton: {
      backgroundColor: "transparent",
    },
    iconContainer: {
      marginRight: 4,
    },
  },
  notificationList: {
    container: {
      // Bump the snackbar above a custom bottom toolbar
      bottom: 96,
    },
  },
};

export const App = () => (
  <OverlayProvider value={{ style: theme }}>
    <Chat client={client}>{/* ... */}</Chat>
  </OverlayProvider>
);

Available slots:

Slot Type What it styles
notification.container ViewStyle The snackbar card
notification.contentContainer ViewStyle Icon + message wrapper
notification.iconContainer ViewStyle Icon bubble
notification.message TextStyle Message text
notification.actionsContainer ViewStyle Wrapper around action buttons
notification.actionButton ViewStyle Each action button
notification.closeButton ViewStyle Dismiss button
notificationList.container ViewStyle The absolute-positioned host

Level 2: Override the Icon

To change the severity-to-icon mapping without touching the rest of the snackbar, override NotificationIcon via WithComponents so every Notification instance picks it up:

import { Chat, useTheme, WithComponents } from "stream-chat-react-native";
import type { NotificationIconProps } from "stream-chat-react-native";
import { CustomError, CustomCheck, CustomInfo, CustomWarning } from "./icons";

const IconBySeverity = {
  error: CustomError,
  success: CustomCheck,
  warning: CustomWarning,
  info: CustomInfo,
} as const;

const CustomNotificationIcon = ({ notification }: NotificationIconProps) => {
  const {
    theme: { semantics },
  } = useTheme();
  const severity = notification.severity;
  const Icon = severity
    ? IconBySeverity[severity as keyof typeof IconBySeverity]
    : null;

  if (!Icon) return null;

  return <Icon height={20} width={20} color={semantics.textOnInverse} />;
};

<WithComponents overrides={{ NotificationIcon: CustomNotificationIcon }}>
  <Chat client={client}>{/* ... */}</Chat>
</WithComponents>;

Level 3: Override the Notification Component

To restructure the snackbar, for example with a different layout, custom close gesture, or brand-specific motion, replace Notification. The component receives the notification object plus these optional props:

Prop Type Description
notification Notification The notification to render. Always present.
Icon ComponentType<NotificationIconProps> The resolved icon component (defaults to the NotificationIcon provided through WithComponents).
onDismiss () => void Call this to dismiss the active notification. NotificationList removes it from the store.
showClose boolean Whether to render the close button. NotificationList sets this to true for persistent notifications.
entryDirection 'bottom' | 'left' | 'right' | 'top' The animation direction the host picked. Useful for stacked transforms.
import { Text, View } from "react-native";
import { Pressable as GHPressable } from "react-native-gesture-handler";
import {
  Chat,
  useTheme,
  useTranslationContext,
  WithComponents,
} from "stream-chat-react-native";
import type { NotificationProps } from "stream-chat-react-native";

const CustomNotification = ({
  Icon,
  notification,
  onDismiss,
  showClose,
}: NotificationProps) => {
  const {
    theme: { semantics },
  } = useTheme();
  const { t } = useTranslationContext();

  const isError = notification.severity === "error";
  const closeVisible = showClose || !notification.duration;

  return (
    <View
      accessibilityLiveRegion={isError ? "assertive" : "polite"}
      accessibilityRole={isError ? "alert" : "summary"}
      style={{
        backgroundColor: semantics.backgroundCoreInverse,
        borderRadius: 16,
        flexDirection: "row",
        padding: 12,
      }}
    >
      {Icon ? <Icon notification={notification} /> : null}
      <Text style={{ color: semantics.textOnInverse, flex: 1, marginLeft: 8 }}>
        {t(notification.message)}
      </Text>
      {notification.actions?.map((action, i) => (
        <GHPressable
          accessibilityRole="button"
          key={`${action.label}-${i}`}
          onPress={action.handler}
          style={{ marginLeft: 8 }}
        >
          <Text style={{ color: semantics.accentPrimary }}>{action.label}</Text>
        </GHPressable>
      ))}
      {closeVisible ? (
        <GHPressable
          accessibilityLabel={t("a11y/Dismiss notification")}
          accessibilityRole="button"
          hitSlop={8}
          onPress={onDismiss}
          style={{ marginLeft: 8 }}
        >
          <Text style={{ color: semantics.textOnInverse }}>✕</Text>
        </GHPressable>
      ) : null}
    </View>
  );
};

<WithComponents overrides={{ Notification: CustomNotification }}>
  <Chat client={client}>{/* ... */}</Chat>
</WithComponents>;

Two accessibility concerns to preserve when overriding:

  • Live region: errors should be assertive (interrupts whatever the screen reader is reading) and other severities should be polite (queued).
  • Dismiss label: pull the label from the translation key a11y/Dismiss notification rather than hardcoding "Close". The SDK ships translations for 12 locales.

The built-in component translates recognized notification type values before rendering. If you replace it, translate notification.message with t(notification.message) and add any type-specific message handling your app needs.

Level 4: Replace NotificationList

Override NotificationList when you need a different presentation entirely, such as a multi-snackbar stack, a banner at the top of the screen, or a platform-native toast that bypasses React Native rendering. Use useNotificationListController if you only need to swap the visual shell while keeping the SDK's selection, timeout, and target-routing logic:

import Animated, { SlideInDown, SlideOutDown } from "react-native-reanimated";
import {
  Chat,
  useComponentsContext,
  useNotificationListController,
  WithComponents,
} from "stream-chat-react-native";
import type { NotificationListProps } from "stream-chat-react-native";

const TopBannerNotificationList = ({
  filter,
  hostId,
  panel,
}: NotificationListProps) => {
  const { Notification } = useComponentsContext();
  const { dismissNotification, notification } = useNotificationListController({
    filter,
    hostId,
    panel,
  });

  if (!notification) return null;

  return (
    <Animated.View
      entering={SlideInDown.duration(220)}
      exiting={SlideOutDown.duration(180)}
      style={{ left: 16, position: "absolute", right: 16, top: 16, zIndex: 20 }}
    >
      <Notification
        notification={notification}
        onDismiss={dismissNotification}
        showClose={!notification.duration}
      />
    </Animated.View>
  );
};

<WithComponents overrides={{ NotificationList: TopBannerNotificationList }}>
  <Chat client={client}>{/* ... */}</Chat>
</WithComponents>;

If you want a stacked snackbar (multiple notifications visible at once), drive the list off useNotifications directly. You're responsible for picking which notifications to show, starting their timeouts, and removing them on dismiss:

import { useEffect } from "react";
import { Pressable, Text, View } from "react-native";
import {
  useNotificationApi,
  useNotifications,
  useNotificationTargetContext,
} from "stream-chat-react-native";

const StackedNotificationList = () => {
  const target = useNotificationTargetContext();
  const notifications = useNotifications({
    filter: (n) => !n.tags?.includes("system"),
    requireTarget: true,
    target,
  });
  const { removeNotification, startNotificationTimeout } = useNotificationApi();

  useEffect(() => {
    notifications.forEach((n) => {
      if (!n.duration) return;
      startNotificationTimeout(n.id);
    });
  }, [notifications, startNotificationTimeout]);

  return (
    <View
      style={{
        bottom: 16,
        left: 16,
        position: "absolute",
        right: 16,
        zIndex: 20,
      }}
    >
      {notifications.slice(-3).map((n) => (
        <Pressable
          accessibilityLabel={n.message}
          accessibilityLiveRegion={
            n.severity === "error" ? "assertive" : "polite"
          }
          accessibilityRole={n.severity === "error" ? "alert" : "summary"}
          key={n.id}
          onPress={() => removeNotification(n.id)}
          style={{
            backgroundColor: "#111827",
            borderRadius: 12,
            marginTop: 8,
            padding: 12,
          }}
        >
          <Text style={{ color: "#f9fafb" }}>{n.message}</Text>
        </Pressable>
      ))}
    </View>
  );
};

Animations

NotificationList accepts an enterFrom prop ('bottom' | 'left' | 'right' | 'top', default 'bottom') that picks one of the SDK's bounded-zoom transitions. The same value flows into Notification.entryDirection, so a custom snackbar can mirror the host's direction.

A single notification can also override the entry direction by setting metadata.entryDirection or origin.context.entryDirection when emitted:

addNotification({
  message: "Pinned to top",
  origin: { emitter: "ChannelListItem", context: { entryDirection: "top" } },
  options: { severity: "success" },
});

For fully custom motion, replace NotificationList and use any Reanimated transition you like.

Mounting Placement

MessageList, ChannelList, and ThreadList mount a NotificationList for you in the built-in layouts. Channel and Thread provide the target context around their message lists. If you've replaced one of those defaults, mount the snackbar yourself inside the same target context:

<Channel channel={channel}>
  <CustomMessageList />
  {/* CustomMessageList doesn't render NotificationList, so mount it here */}
  <NotificationList panel="channel" />
</Channel>

NotificationList also accepts bottomOffset and topOffset props for pushing the snackbar above a floating composer or below a sticky header. The default MessageList already wires bottomOffset to the floating message-input height.

Accessibility

The built-in Notification ships with the right accessibility semantics out of the box:

  • Live region: assertive for severity: 'error', polite for everything else.
  • Role: alert for errors, summary for other severities.
  • The dismiss button has a translatable accessibilityLabel from the a11y/Dismiss notification translation key.
  • NotificationList is labelled with the a11y/Notifications key.
  • Action buttons have accessibilityRole='button'.

When overriding Notification or NotificationList, preserve these. Adopt the same live-region rules and use the existing translation keys rather than hardcoding strings. The SDK already translates them across 12 locales.

For animations, respect the user's reduced-motion preference. The SDK's useReducedMotionPreference hook returns true when the OS-level setting is enabled. In your custom snackbar, fall back to instant transitions:

import { useReducedMotionPreference } from "stream-chat-react-native";

const reduceMotion = useReducedMotionPreference();
const duration = reduceMotion ? 0 : 200;

Mirror these patterns when shipping a custom snackbar so screen reader and reduced-motion users get the same experience as the rest of the app.