Notifications accessibility
Notifications reach users of screen readers without them looking at the screen. Use this page to know what VoiceOver and TalkBack announce for a notification, and how to have more of them read. Accessibility across the SDK is on Accessibility.
Notification announcements
Notifications are the messages the SDK and your code publish to client.notifications, for example with addNotification from useNotificationApi(). Displaying notifications shows where they appear and how they look.
The default Notification carries its own accessibility props:
- Android. It is a live region,
assertivefor an error andpolitefor every other severity, so TalkBack reads it when it appears. This doesn't depend onaccessibility.enabledinOverlayProvider. - iOS. VoiceOver has no live regions, so nothing is read when a notification appears. A user who moves focus to its text hears it.
- Role. An error has the
alertrole and every other severity thesummaryrole. - Close button. A notification without a duration, or one shown with
showClose, has a close button labeled "Dismiss notification". - Action buttons. Each action is a button, labeled with the action's
label.
The SDK's NotificationAnnouncer, which Channel and Thread render, announces the connection going down and coming back, and not notifications. Connection announcements covers it.
A system notification is shown by no list, so neither platform reads it unless the banner your app renders for it is a live region of its own, or your app announces it, as Announcing notifications yourself shows.
Time to hear and act
A notification leaves after its duration, which is three seconds by default for a notification with a severity, and at least five when it has action buttons. A screen-reader user may not reach an action button in that time. Publish a notification with an action using duration: 0, so it stays until the user acts or dismisses it, and the list shows a close button. The SDK does this for the notification that asks for microphone permission:
import { useNotificationApi } from "stream-chat-react-native";
export const useNotifyRetry = (retry: () => void) => {
const { addNotification } = useNotificationApi();
return () =>
addNotification({
message: "Could not save your changes",
options: {
actions: [{ handler: retry, label: "Try again" }],
duration: 0,
severity: "error",
type: "app:changes:save:failed",
},
origin: { emitter: "useNotifyRetry" },
});
};Publishing notifications explains durations.
Announcing notifications yourself
To have notifications read on iOS, or to read the ones no list shows, announce them with useAccessibilityAnnouncer(). It calls AccessibilityInfo.announceForAccessibility() on both platforms, and does nothing unless OverlayProvider has accessibility={{ enabled: true }}.
This component reads every new notification. On Android it leaves out the ones a list shows, which the live region already reads, and keeps the system ones:
import { useEffect, useRef } from "react";
import { Platform } from "react-native";
import type { Notification } from "stream-chat";
import {
hasSystemNotificationTag,
useAccessibilityAnnouncer,
useNotifications,
useTranslationContext,
} from "stream-chat-react-native";
export const NotificationScreenReaderAnnouncer = () => {
const announce = useAccessibilityAnnouncer();
const { t } = useTranslationContext();
const notifications = useNotifications();
const seenIdsRef = useRef<Set<string> | null>(null);
useEffect(() => {
// The notifications already in the store when this mounts aren't read.
if (!seenIdsRef.current) {
seenIdsRef.current = new Set(notifications.map(({ id }) => id));
return;
}
const seenIds = seenIdsRef.current;
notifications.forEach((notification: Notification) => {
if (seenIds.has(notification.id)) return;
seenIds.add(notification.id);
if (Platform.OS === "android" && !hasSystemNotificationTag(notification))
return;
// The message of a notification stream-chat publishes is English.
const message =
notification.type === "api:attachment:upload:failed"
? t("notifications.attachmentUploadFailed.error")
: notification.message;
announce(
message,
notification.severity === "error" ? "assertive" : "polite",
);
});
}, [announce, notifications, t]);
return null;
};Render it once, inside Chat, for example next to your navigator. Two copies read every notification twice.
The text of a notification is its message. The SDK's components translate it before publishing, and so does your code, as on Notifications text. The notifications stream-chat publishes carry untranslated English, which the Notification component replaces with a translation key as it renders, by the notification's type. The announcer above does the same for one type, and a complete one maps the types your app can see, with the keys listed in Keys by type.
useNotifications() returns every notification in the store, system ones included. Pass it a filter, declared at module scope, to read fewer.
Localizing notification accessibility
The accessible names are translation keys like the rest of the UI:
notifications.dismiss.accessibilityLabelis the close button, "Dismiss notification".notifications.list.accessibilityLabelis the label of the list container, "Notifications".a11y.connection.offline.accessibilityLabel,a11y.connection.reconnecting.accessibilityLabelanda11y.connection.connected.accessibilityLabelare the connection announcements.
Localizing announcements explains how announcements are translated.