ワンクリックで
accessibility
// Maintain VoiceOver/TalkBack-focused accessibility in stream-chat-react-native. Use when changing interactive components, gestures, modals, lists, media controls, notifications, focus behavior, or live announcements.
// Maintain VoiceOver/TalkBack-focused accessibility in stream-chat-react-native. Use when changing interactive components, gestures, modals, lists, media controls, notifications, focus behavior, or live announcements.
| name | accessibility |
| description | Maintain VoiceOver/TalkBack-focused accessibility in stream-chat-react-native. Use when changing interactive components, gestures, modals, lists, media controls, notifications, focus behavior, or live announcements. |
Use this skill whenever code changes can affect screen-reader users (VoiceOver on iOS, TalkBack on Android), gesture-driven flows, focus behavior, motion preferences, or semantic React Native accessibility props.
Pressable, TextInput, Switch, Image directly. Use accessibilityRole only when native semantics cannot represent the widget (menu, menuitem, progressbar, radio, checkbox, article, alert, tablist, tab).accessibilityLabel/accessibilityHint/announcement strings. For SDK Button, pass accessibilityLabelKey='a11y/...' (and accessibilityLabelParams when needed). For non-Button components, use useA11yLabel('a11y/...', params) or t('a11y/...') directly when you don't need the disabled-state short-circuit. Add the key to all 12 locale files in package/src/i18n/.useAccessibilityContext().enabled. A11y is opt-in. New listeners, subscriptions, and announcer mounts must be no-ops when enabled is false. New accessibilityRole/accessibilityState props are fine to render unconditionally — they cost ~zero.Pressable inside Pressable. Mark inner decorative views with accessibilityElementsHidden (iOS) + importantForAccessibility='no-hide-descendants' (Android) so the parent carries the label.accessibilityLabel on the wrapper, and the SVG icon should be hidden.WithComponents) must continue to work.package/src/a11y/ (utilities + low-level hooks).package/src/components/Accessibility/ (NotificationAnnouncer, useAccessibilityAnnouncer, useIncomingMessageAnnouncements).package/src/contexts/accessibilityContext/, mounted by OverlayProvider.a11y/* keys in all 12 locale JSONs (en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr).Platform.OS or useResolvedModalAccessibilityProps. Don't duplicate the file — RN doesn't need .ios.tsx/.android.tsx splits for a11y.__tests__/ folder; use @testing-library/react-native semantic queries (getByRole, getByLabelText).import { Button, useA11yLabel } from 'stream-chat-react-native';
const labelParams = useMemo(() => ({ count, emoji }), [count, emoji]);
const label = useA11yLabel('a11y/Reaction {{emoji}} by {{count}} users', labelParams);
<Pressable accessibilityLabel={label} accessibilityRole='button' accessibilityState={{ selected }} />
<Button accessibilityLabelKey='a11y/Send message' iconOnly {...buttonProps} />
useA11yLabel returns undefined when accessibility.enabled is false, so the t() call is skipped on hot list paths.
Button centralizes this same behavior for SDK-owned buttons. In SDK code, pass the key/params only. When migrating a released button that already had an accessibilityLabel, make the new translation resolve to the same existing label unless the change is intentionally breaking.
For composite labels (sender + timestamp + body + reactions summary), use composeAccessibilityLabel(...parts) from package/src/a11y/a11yUtils.ts — it filters out empty/null parts and joins with , so screen readers add a brief pause.
Two complementary mechanisms:
useAccessibilityAnnouncer() returns (message, priority?) => void. Same shape as stream-chat-react's useAriaLiveAnnouncer. Wraps AccessibilityInfo.announceForAccessibility with sequence/debounce so repeat announcements still re-announce.accessibilityLiveRegion="polite" (Android only) on a View that re-renders when its label changes.Use useAnnounceOnStateChange(message, { debounceMs, priority }) for transitions (AI typing, indicators) — it dedups consecutive same-message calls and applies a default 250ms debounce.
For incoming messages: use useIncomingMessageAnnouncements({ channel, ownUserId, activeThreadId, threadList }). It throttles to 1 announcement per second, batches multi-message bursts, and bounds memory at 500 announced ids.
Use useResolvedModalAccessibilityProps() and spread the result on the modal root:
const a11yProps = useResolvedModalAccessibilityProps();
<Animated.View {...a11yProps} style={...}>
{/* ... */}
</Animated.View>
This returns:
{ accessibilityViewIsModal: true }{ importantForAccessibility: 'yes' }enabled is false: {}After opening, set initial focus via AccessibilityInfo.setAccessibilityFocus(findNodeHandle(titleRef.current)) deferred behind requestAnimationFrame so the a11y tree has settled.
Mobile gestures (long-press, hold-to-record, pinch/pan) must have a tap-equivalent for SR users. Read the component's mode flag from AccessibilityConfig:
const { audioRecorderTapMode } = useAccessibilityContext();
const screenReaderOn = useScreenReaderEnabled();
const useTapMode =
audioRecorderTapMode === 'always' ||
(audioRecorderTapMode === 'auto' && screenReaderOn);
Three-state semantics: 'auto' (swap when SR is on), 'always' (swap for everyone), 'never' (integrator handles).
const reduceMotion = useReducedMotionPreference();
const transitionDuration = reduceMotion ? 0 : 250;
Disable spring animations and limit fade durations when this is true.
accessibilityLabel strings inside component code. For SDK Button, use accessibilityLabelKey='a11y/...'; otherwise use useA11yLabel('a11y/...') or t('a11y/...').<Pressable><Pressable> causes VO to stop on each. Mark the outer accessible={false} or the inner accessibilityElementsHidden.AccessibilityInfo events when enabled is false — wastes a listener slot. The provided hooks already gate on this; mirror that pattern.useScreenReaderEnabled() inside list items — toggling SR re-renders every item. Only subscribe in components that actually swap UI on SR (AudioRecorder, ImageGallery, Message's alternative-actions button).useResolvedModalAccessibilityProps + correct accessibilityRole='alert').AccessibilityInfo polyfill state in tests without restoring — use the mock-builder helpers in package/src/mock-builders/accessibility/ (or jest.mock the module) and reset between tests.Minimum:
__tests__/.@testing-library/react-native semantic queries: getByRole, getByLabelText, getByA11yState, getByA11yValue.Recommended for non-trivial changes:
<OverlayProvider accessibility={{ enabled: true, forceScreenReaderMode: true }}> and assert the accessible variant renders.<OverlayProvider accessibility={{ enabled: false }}> and assert the legacy behavior is unchanged (no extra buttons, no listeners).accessibilityRole only when necessaryButton accessibilityLabelKey='a11y/...' or useA11yLabel('a11y/...') (not hardcoded)a11y/* key to all 12 locale JSONs and ran yarn build-translationsaccessibilityState for stateful widgets (disabled, selected, checked, busy, expanded)accessibilityElementsHidden / importantForAccessibility='no-hide-descendants')useResolvedModalAccessibilityPropsuseAccessibilityContext().enabled<OverlayProvider accessibility={{ enabled: true, forceScreenReaderMode: true }}> and enabled: falseyarn lint passes (validate-translations enforces non-empty a11y/* keys)yarn tsc --noEmit passes (RN's a11y prop types are strict about boolean | null | undefined)package/src/contexts/accessibilityContext/AccessibilityContext.tsx — config schema + provider + imperative announcer context.package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts — port of stream-chat-react's hook.package/src/a11y/hooks/useA11yLabel.ts — translated-label-or-undefined.package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts — modal a11y props.package/src/components/ui/Avatar/Avatar.tsx — example of name + useA11yLabel usage.package/src/components/UIComponents/BottomSheetModal.tsx — example of useResolvedModalAccessibilityProps.package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx — example of useAnnounceOnStateChange.API shapes mirror stream-chat-react#3146:
useAccessibilityAnnouncer ≈ React's useAriaLiveAnnounceruseIncomingMessageAnnouncements — same params, same throttle/batch logica11y/* i18n namespace shared<NotificationAnnouncer /> — same component name, connection-state-only on RN since RN has no shared notifications queueWhen changing one SDK's a11y API, mirror it in the other where the platforms agree.