name: accessibility description: Maintain WCAG-focused accessibility in stream-chat-react. Use when changing interactive components, dialogs, menus, forms, media controls, notifications, focus behavior, keyboard flows, aria attributes, or screen-reader announcements.
Accessibility Maintenance (stream-chat-react)
Use this skill whenever code changes can affect keyboard users, screen readers, focus behavior, motion preferences, or semantic HTML/ARIA.
Non-negotiable rules
- Prefer native semantics first (
button,input,label,img, etc.). Use ARIA roles only when native semantics cannot represent the widget. - Do not add hardcoded English accessibility labels. Use i18n keys (
t('aria/...')) for user-facingaria-label/aria-description/announcement strings. - Keep one focusable interactive target per action. Avoid duplicate focus stops and nested-interactive patterns.
- If a control is keyboard-activatable, support Enter/Space behavior and visible focus.
- Decorative visuals stay hidden from AT (
aria-hidden,focusable="false"for SVG icons). - Keep changes additive and backward-compatible for SDK consumers.
Where to put what
- Cross-cutting accessibility decisions and scope changes
specs/wcag-compliance/decisions.md(append-only decision log)specs/wcag-compliance/plan.mdandspecs/wcag-compliance/state.json(task tracking)
- Shared a11y primitives
src/components/Accessibility/(AriaLiveRegion, announcer hooks, notification announcer)src/components/VisuallyHidden/
- Global reduced-motion/focus behavior styles
src/styling/accessibility.scsssrc/styling/base.scss(shared focus tokens)
- Component-level semantics and keyboard handling
- in the component itself under
src/components/**
- in the component itself under
- Translations for new
aria/...keys- all locale files in
src/i18n/*.json(never leave empty values)
- all locale files in
- Tests
- nearest component test folder:
src/components/**/__tests__/ - include
jest-axechecks for semantic/ARIA-sensitive changes
- nearest component test folder:
Patterns to follow
1) Accessible names and descriptions
- Prefer
aria-labelledbywhen visible label text exists. - Use
aria-labelonly as fallback when label-by-id is not available. - For dialog surfaces:
- provide
roleandaria-modalfor modal behavior - wire
aria-labelledbyandaria-describedbyto visible title/description nodes
- provide
- For images:
- always render
alt(''when decorative)
- always render
2) Keyboard behavior
- Native controls: rely on native keyboard behavior whenever possible.
- Non-native interactive wrappers (only when unavoidable):
role="button"tabIndex={0}onKeyDownfor Enter/Space activation- prevent default on Space when needed to avoid scroll side-effects
- Menus/listboxes/tabs:
- use role-appropriate child roles (
menu/menuitem,listbox/option,tablist/tab/tabpanel) - keep role/attribute combinations valid (example:
menuitemradiowitharia-checked)
- use role-appropriate child roles (
3) Live regions and announcements
- Prefer centralized announcers over ad-hoc
aria-livescattered across components:AriaLiveRegion+useAriaLiveAnnouncerNotificationAnnouncer
- Use
politefor non-urgent updates;assertivefor urgent/error updates. - For repeated announcements, clear then set message (small delay) to force re-announcement.
- For modals, do not use live regions for static body content; rely on correct dialog semantics + focus management.
4) Focus management
- Maintain visible focus indicators (do not remove outlines without replacement).
- When trapping focus in dialogs, ensure focus enters the dialog and is restored on close.
- After closing transient dialogs/popovers, restore focus to the invoking trigger when expected.
5) Motion preferences
- Respect
prefers-reduced-motionin both CSS and JS behavior:- CSS transitions/animations minimized in
accessibility.scss - JS-driven scrolling/animation behavior downgraded to non-smooth where needed
- CSS transitions/animations minimized in
ARIA attribute guardrails
- Use ARIA as contract, not decoration:
- if role implies state, provide matching state attribute (
aria-selected,aria-checked, etc.) only when valid for that role - never attach unsupported attributes to roles just to satisfy a visual state
- if role implies state, provide matching state attribute (
aria-hiddenis for decorative/non-essential content only, never for focusable controls.- Icon-only controls must carry an accessible name on the control element itself.
i18n rules for accessibility text
- New accessibility labels/announcements must use
t('aria/...')or established translation topics. - Add keys to all locales in
src/i18n/*.json. - For dynamic values, always use i18n interpolation syntax (for example
t('aria/{{count}} new messages', { count })or equivalent existing key shape), never string concatenation. - Run translation validation/lint flow; no empty translation values.
Testing requirements per accessibility change
Minimum:
- unit tests for new keyboard/focus/semantics behavior in nearest
__tests__folder - one
jest-axeassertion for components where semantics changed
Recommended:
- regression tests for:
- Enter/Space activation
- role/state attributes
- focus restore on close
- reduced-motion behavior where logic branches in JS
Execution workflow (copy this checklist)
- Identify the interaction type (button/menu/dialog/listbox/form/slider/live region)
- Choose native element first; fall back to ARIA only if necessary
- Add or correct label wiring (
aria-labelledbypreferred,aria-labelfallback) - Verify keyboard path (Tab + Enter/Space + arrow keys where pattern requires)
- Verify focus visibility and focus restore behavior
- Ensure decorative visuals are hidden from AT and icon-only controls are named
- Add/update i18n keys for new accessibility text across all locales
- Add/update tests (
jest-axewhere semantics changed) - Append rationale to
specs/wcag-compliance/decisions.mdfor cross-cutting decisions
Common mistakes to avoid
- Hardcoded English
aria-labelvalues in component code. - Adding
tabIndex/roles to containers that only capture backdrop clicks. - Creating two focusable wrappers for one action path.
- Introducing invalid role/attribute pairs (for example
aria-selectedon plain buttons). - Using live regions to force modal text announcement instead of fixing dialog semantics.