cometchat-a11y

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Purpose

目的

Accessibility for CometChat integrations. Out-of-the-box, the UI Kit components are mostly accessible — the kit's own buttons, inputs, and lists ship with semantic markup. Production gaps appear in the wiring around the kit: custom call surfaces, navigation, focus management on screen transitions, and contrast in custom themes.
Target: WCAG 2.1 AA. The skill writes code that meets this baseline.

CometChat集成的无障碍访问优化。UI Kit组件本身大多具备无障碍访问能力——套件自带的按钮、输入框和列表都采用了语义化标记。生产环境中的问题往往出现在套件的外围配置中:自定义通话界面、导航逻辑、屏幕切换时的焦点管理,以及自定义主题的对比度问题。
目标:WCAG 2.1 AA。本指南提供的代码将满足这一基准要求。

The five gaps that trip integrations (any family)

困扰各框架集成的五大无障碍问题

  1. Color contrast in custom themes. A brand color picked for "looks nice in the brand book" may be 3.2:1 against the text — fails AA's 4.5:1 minimum.
  2. Focus management on chat screen entry. Tab/screen reader user lands on the chat screen but focus stays on the previous trigger button. They have to manually navigate into the message list every time.
  3. No live region announcement for new messages. Screen reader users don't know a new message arrived unless they navigate the message list and hear the new item.
  4. Keyboard-only users can't navigate the conversation list. Click handlers bound to
    <div>
    instead of
    <button>
    skip keyboard events.
  5. Reduced-motion users see decorative animations. Typing-indicator dots, message bubble entrance animations, transition effects — should respect
    prefers-reduced-motion
    .
This skill addresses each one across families.

  1. 自定义主题的颜色对比度问题。为了“符合品牌手册视觉效果”选择的品牌色,其与文本的对比度可能仅为3.2:1——未达到AA标准要求的最低4.5:1。
  2. 进入聊天屏幕时的焦点管理缺失。使用Tab键或屏幕阅读器的用户进入聊天屏幕后,焦点仍停留在之前的触发按钮上,每次都需要手动导航到消息列表。
  3. 新消息无实时区域播报。屏幕阅读器用户无法知晓新消息到达,除非主动导航到消息列表并收听新内容。
  4. 纯键盘用户无法导航对话列表。点击事件绑定在
    <div>
    而非
    <button>
    上,导致键盘事件被忽略。
  5. 开启减少动画偏好的用户仍会看到装饰性动画。输入指示器圆点、消息气泡入场动画、过渡效果等——均应遵循
    prefers-reduced-motion
    设置。
本指南针对上述问题提供跨框架的解决方案。

1. Color contrast — the theme audit

1. 颜色对比度——主题审计

CometChat themes are CSS variables (web/RN) or color tokens (native/Flutter). Override a single color and you might fail AA.
CometChat主题采用CSS变量(Web/React Native)或颜色令牌(原生/Flutter)。仅覆盖单个颜色就可能导致不符合AA标准。

Web / Angular — CSS variable contrast check

Web / Angular —— CSS变量对比度检查

ts
// scripts/check-contrast.ts (run in CI or as a one-shot)
function contrastRatio(hex1: string, hex2: string): number {
  const luminance = (hex: string) => {
    const rgb = hex.match(/\w\w/g)!.map(c => parseInt(c, 16) / 255).map(c =>
      c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
    );
    return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
  };
  const l1 = luminance(hex1);
  const l2 = luminance(hex2);
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}

// Pull the values from your CSS variables
const fg = getComputedStyle(document.documentElement).getPropertyValue("--cometchat-text-color").trim();
const bg = getComputedStyle(document.documentElement).getPropertyValue("--cometchat-background-color").trim();
const ratio = contrastRatio(fg, bg);
if (ratio < 4.5) {
  console.warn(`Text/background contrast ${ratio.toFixed(2)}:1 fails WCAG AA (need ≥4.5:1)`);
}
In CI, add this to your test suite. The skill writes a starter version into
tests/a11y/contrast.test.ts
.
ts
// scripts/check-contrast.ts (在CI中运行或作为一次性脚本)
function contrastRatio(hex1: string, hex2: string): number {
  const luminance = (hex: string) => {
    const rgb = hex.match(/\w\w/g)!.map(c => parseInt(c, 16) / 255).map(c =>
      c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
    );
    return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
  };
  const l1 = luminance(hex1);
  const l2 = luminance(hex2);
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}

// 从CSS变量中获取值
const fg = getComputedStyle(document.documentElement).getPropertyValue("--cometchat-text-color").trim();
const bg = getComputedStyle(document.documentElement).getPropertyValue("--cometchat-background-color").trim();
const ratio = contrastRatio(fg, bg);
if (ratio < 4.5) {
  console.warn(`文本/背景对比度 ${ratio.toFixed(2)}:1 未达到WCAG AA标准(需≥4.5:1)`);
}
在CI流程中,将此脚本添加到测试套件中。本指南会提供初始版本并写入
tests/a11y/contrast.test.ts

React Native / native / Flutter — manual audit at theme-design time

React Native / 原生 / Flutter —— 主题设计阶段的手动审计

Use a contrast-checker tool (browser extensions, https://webaim.org/resources/contrastchecker/) on the theme tokens before shipping. There's no runtime DOM to audit on native.
The kit's default theme tokens pass AA. Custom palettes need the audit.
Common fail: brand purple #6750A4 against white background = 6.6:1 (passes). Same purple against
#F0F0F0
light gray = 5.7:1 (passes). Same purple against
#999999
muted gray = 2.8:1 (FAILS). Watch for muted backgrounds in dark-mode toggles, secondary buttons, and "subtle" surfaces.

套件默认主题令牌符合AA标准。自定义调色板需要进行审计。
常见失败案例:品牌紫色#6750A4与白色背景的对比度为6.6:1(通过);与浅灰色
#F0F0F0
的对比度为5.7:1(通过);与暗灰色
#999999
的对比度为2.8:1(失败)。需注意深色模式切换、次要按钮和“低关注度”界面中的暗色调背景。

2. Focus management on chat screen entry

2. 进入聊天屏幕时的焦点管理

When the user navigates to a chat screen (clicked a conversation, opened the chat tab, accepted a deep link), focus should land on a meaningful control — usually the message composer or the latest message.
当用户导航到聊天屏幕(点击对话、打开聊天标签、通过深度链接进入)时,焦点应落在有意义的控件上——通常是消息输入框或最新消息。

React (web)

React(Web)

tsx
import { useEffect, useRef } from "react";

export function ChatScreen() {
  const composerRef = useRef<HTMLElement>(null);

  useEffect(() => {
    // After mount + animations, focus the composer
    const timer = setTimeout(() => {
      composerRef.current?.focus();
    }, 100);
    return () => clearTimeout(timer);
  }, []);

  return (
    <div>
      <CometChatMessageHeader />
      <CometChatMessageList />
      <CometChatMessageComposer ref={composerRef} />
    </div>
  );
}
The kit's
CometChatMessageComposer
accepts a forwarded ref in v6; if not, query for the input via
composerRef.current?.querySelector("input, [contenteditable]")?.focus()
.
tsx
import { useEffect, useRef } from "react";

export function ChatScreen() {
  const composerRef = useRef<HTMLElement>(null);

  useEffect(() => {
    // 挂载完成并结束动画后,聚焦输入框
    const timer = setTimeout(() => {
      composerRef.current?.focus();
    }, 100);
    return () => clearTimeout(timer);
  }, []);

  return (
    <div>
      <CometChatMessageHeader />
      <CometChatMessageList />
      <CometChatMessageComposer ref={composerRef} />
    </div>
  );
}
v6版本的套件
CometChatMessageComposer
支持转发ref;若不支持,可通过
composerRef.current?.querySelector("input, [contenteditable]")?.focus()
获取输入框并聚焦。

React Native

React Native

tsx
import { useRef, useEffect } from "react";
import { findNodeHandle, AccessibilityInfo } from "react-native";

export function ChatScreen() {
  const composerRef = useRef(null);

  useEffect(() => {
    const handle = findNodeHandle(composerRef.current);
    if (handle) {
      AccessibilityInfo.setAccessibilityFocus(handle);
    }
  }, []);

  return (
    <View>
      <CometChatMessageHeader />
      <CometChatMessageList />
      <CometChatMessageComposer ref={composerRef} />
    </View>
  );
}
tsx
import { useRef, useEffect } from "react";
import { findNodeHandle, AccessibilityInfo } from "react-native";

export function ChatScreen() {
  const composerRef = useRef(null);

  useEffect(() => {
    const handle = findNodeHandle(composerRef.current);
    if (handle) {
      AccessibilityInfo.setAccessibilityFocus(handle);
    }
  }, []);

  return (
    <View>
      <CometChatMessageHeader />
      <CometChatMessageList />
      <CometChatMessageComposer ref={composerRef} />
    </View>
  );
}

Angular

Angular

ts
@Component({...})
export class ChatComponent implements AfterViewInit {
  @ViewChild("composer") composer!: ElementRef;

  ngAfterViewInit() {
    setTimeout(() => this.composer.nativeElement.focus(), 100);
  }
}
ts
@Component({...})
export class ChatComponent implements AfterViewInit {
  @ViewChild("composer") composer!: ElementRef;

  ngAfterViewInit() {
    setTimeout(() => this.composer.nativeElement.focus(), 100);
  }
}

Native Android (Kotlin)

原生Android(Kotlin)

kotlin
override fun onResume() {
  super.onResume()
  composerView.requestFocus()
  composerView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
kotlin
override fun onResume() {
  super.onResume()
  composerView.requestFocus()
  composerView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}

Native iOS (Swift)

原生iOS(Swift)

swift
override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  UIAccessibility.post(notification: .screenChanged, argument: composerView)
}
swift
override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  UIAccessibility.post(notification: .screenChanged, argument: composerView)
}

Flutter

Flutter

dart
final FocusNode _composerFocus = FocusNode();


void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _composerFocus.requestFocus();
  });
}

// Then on the composer widget: focusNode: _composerFocus

dart
final FocusNode _composerFocus = FocusNode();


void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _composerFocus.requestFocus();
  });
}

// 然后在输入框组件上设置:focusNode: _composerFocus

3. Live region for new messages

3. 新消息的实时区域

Screen reader users need an audible announcement when a new message arrives — otherwise they have to navigate to the message list and re-read it.
屏幕阅读器用户需要在新消息到达时听到语音播报——否则他们必须导航到消息列表重新查看。

Web / Angular — ARIA live region

Web / Angular —— ARIA实时区域

html
<!-- A visually-hidden region that screen readers announce -->
<div
  aria-live="polite"
  aria-atomic="true"
  style="position: absolute; left: -9999px; height: 1px; width: 1px; overflow: hidden;"
  id="message-announcer"></div>
ts
// Listen for new messages and announce
import { CometChat } from "@cometchat/chat-sdk-javascript";

const listenerId = "a11y-message-announcer";
CometChat.addMessageListener(listenerId, new CometChat.MessageListener({
  onTextMessageReceived: (msg: CometChat.TextMessage) => {
    const senderName = msg.getSender().getName();
    const text = msg.getText();
    const region = document.getElementById("message-announcer");
    if (region) {
      // Clearing first ensures the same text re-announces
      region.textContent = "";
      setTimeout(() => {
        region.textContent = `New message from ${senderName}: ${text}`;
      }, 100);
    }
  },
}));
aria-live="polite"
waits for the user to finish speaking before announcing. Use
aria-live="assertive"
only for urgent messages (like incoming calls) — too aggressive for chat.
html
<!-- 屏幕阅读器可播报的视觉隐藏区域 -->
<div
  aria-live="polite"
  aria-atomic="true"
  style="position: absolute; left: -9999px; height: 1px; width: 1px; overflow: hidden;"
  id="message-announcer"></div>
ts
// 监听新消息并播报
import { CometChat } from "@cometchat/chat-sdk-javascript";

const listenerId = "a11y-message-announcer";
CometChat.addMessageListener(listenerId, new CometChat.MessageListener({
  onTextMessageReceived: (msg: CometChat.TextMessage) => {
    const senderName = msg.getSender().getName();
    const text = msg.getText();
    const region = document.getElementById("message-announcer");
    if (region) {
      // 先清空内容,确保相同文本可重复播报
      region.textContent = "";
      setTimeout(() => {
        region.textContent = `来自${senderName}的新消息:${text}`;
      }, 100);
    }
  },
}));
aria-live="polite"
会等待用户完成当前操作后再进行播报。仅在紧急消息(如来电)时使用
aria-live="assertive"
——聊天场景下使用会过于突兀。

React Native

React Native

ts
import { AccessibilityInfo } from "react-native";

CometChat.addMessageListener(listenerId, new CometChat.MessageListener({
  onTextMessageReceived: (msg) => {
    const text = `New message from ${msg.getSender().getName()}: ${msg.getText()}`;
    AccessibilityInfo.announceForAccessibility(text);
  },
}));
ts
import { AccessibilityInfo } from "react-native";

CometChat.addMessageListener(listenerId, new CometChat.MessageListener({
  onTextMessageReceived: (msg) => {
    const text = `来自${msg.getSender().getName()}的新消息:${msg.getText()}`;
    AccessibilityInfo.announceForAccessibility(text);
  },
}));

Native Android / iOS / Flutter

原生Android / iOS / Flutter

Each platform has an equivalent — Android
View.announceForAccessibility(text)
, iOS
UIAccessibility.post(notification: .announcement, argument: text)
, Flutter
SemanticsService.announce(text, TextDirection.ltr)
. Same shape; the SDK callback is the trigger.

每个平台都有对应的实现——Android使用
View.announceForAccessibility(text)
,iOS使用
UIAccessibility.post(notification: .announcement, argument: text)
,Flutter使用
SemanticsService.announce(text, TextDirection.ltr)
。逻辑一致,均通过SDK回调触发。

4. Keyboard navigation

4. 键盘导航

The kit's components are keyboard-accessible by default. Custom wrapping is what breaks it.
套件组件默认支持键盘访问。自定义封装是导致功能失效的常见原因。

Anti-pattern —
<div onClick>
for clickable items

反模式——为可点击元素使用
<div onClick>

tsx
// ✗ WRONG — keyboard users can't activate
<div onClick={() => openConversation(c)}>{c.name}</div>

// ✓ RIGHT — `<button>` is keyboard + screen-reader native
<button onClick={() => openConversation(c)}>{c.name}</button>

// ✓ ALSO RIGHT — div with explicit ARIA + keyboard handlers
<div
  role="button"
  tabIndex={0}
  onClick={() => openConversation(c)}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      openConversation(c);
    }
  }}
>
  {c.name}
</div>
tsx
// ✗ 错误——键盘用户无法激活
<div onClick={() => openConversation(c)}>{c.name}</div>

// ✓ 正确——`<button>`原生支持键盘+屏幕阅读器
<button onClick={() => openConversation(c)}>{c.name}</button>

// ✓ 另一种正确方式——添加明确ARIA属性和键盘事件的div
<div
  role="button"
  tabIndex={0}
  onClick={() => openConversation(c)}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      openConversation(c);
    }
  }}
>
  {c.name}
</div>

Skip links

跳转链接

For long conversation lists, add a skip-to-message-composer link:
html
<a href="#message-composer" class="skip-link">Skip to message composer</a>
css
.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 999;
}
.skip-link:focus {
  left: 0;
  top: 0;
  background: white;
  padding: 8px;
}
The kit's components already include skip links where applicable; custom wrapping should preserve them.
对于长对话列表,添加跳转到消息输入框的链接:
html
<a href="#message-composer" class="skip-link">跳转到消息输入框</a>
css
.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 999;
}
.skip-link:focus {
  left: 0;
  top: 0;
  background: white;
  padding: 8px;
}
套件组件已在适用场景中包含跳转链接;自定义封装时应保留该功能。

Keyboard shortcuts

键盘快捷键

For productivity apps:
ts
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    // Cmd/Ctrl + K → focus search
    if ((e.metaKey || e.ctrlKey) && e.key === "k") {
      e.preventDefault();
      searchRef.current?.focus();
    }
    // Escape → close any open thread / modal
    if (e.key === "Escape") {
      closeOpenThread();
    }
  };
  window.addEventListener("keydown", handler);
  return () => window.removeEventListener("keydown", handler);
}, []);
Document the shortcuts in your in-app help — discoverability matters.

针对生产力应用:
ts
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    // Cmd/Ctrl + K → 聚焦搜索框
    if ((e.metaKey || e.ctrlKey) && e.key === "k") {
      e.preventDefault();
      searchRef.current?.focus();
    }
    // Escape → 关闭所有打开的线程/模态框
    if (e.key === "Escape") {
      closeOpenThread();
    }
  };
  window.addEventListener("keydown", handler);
  return () => window.removeEventListener("keydown", handler);
}, []);
在应用内帮助文档中说明快捷键——可发现性至关重要。

5. Reduced motion

5. 减少动画

Animations help most users; they cause physical discomfort or distraction for users with vestibular disorders, ADHD, or who simply prefer less movement. WCAG 2.1 AA requires honoring the OS preference.
动画对大多数用户有帮助,但会给前庭障碍、ADHD用户或偏好少动画的用户带来身体不适或干扰。WCAG 2.1 AA标准要求遵循系统偏好设置。

Web / Angular — CSS

Web / Angular —— CSS

css
@media (prefers-reduced-motion: reduce) {
  /* Disable kit animations + your custom ones */
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
css
@media (prefers-reduced-motion: reduce) {
  /* 禁用套件动画及自定义动画 */
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

React Native

React Native

ts
import { AccessibilityInfo } from "react-native";

const [reduceMotion, setReduceMotion] = useState(false);

useEffect(() => {
  AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
  const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", setReduceMotion);
  return () => sub.remove();
}, []);

// In animations
<Animated.View
  style={{
    transform: [{ scale: reduceMotion ? 1 : animatedValue }],
  }}
/>
ts
import { AccessibilityInfo } from "react-native";

const [reduceMotion, setReduceMotion] = useState(false);

useEffect(() => {
  AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
  const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", setReduceMotion);
  return () => sub.remove();
}, []);

// 在动画中使用
<Animated.View
  style={{
    transform: [{ scale: reduceMotion ? 1 : animatedValue }],
  }}
/>

Native iOS

原生iOS

swift
let reduceMotion = UIAccessibility.isReduceMotionEnabled
if !reduceMotion {
  UIView.animate(withDuration: 0.3) { ... }
} else {
  // Apply final state without animation
}
swift
let reduceMotion = UIAccessibility.isReduceMotionEnabled
if !reduceMotion {
  UIView.animate(withDuration: 0.3) { ... }
} else {
  // 直接应用最终状态,不使用动画
}

Native Android

原生Android

kotlin
val reduceMotion = Settings.Global.getFloat(
  contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
) == 0.0f
kotlin
val reduceMotion = Settings.Global.getFloat(
  contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
) == 0.0f

Flutter

Flutter

dart
final reduceMotion = MediaQuery.of(context).disableAnimations;
The skill defaults to wrapping the typing indicator + message-bubble entrance animations in reduce-motion guards. Other kit animations need similar treatment if you've customized them.

dart
final reduceMotion = MediaQuery.of(context).disableAnimations;
本指南默认将输入指示器和消息气泡入场动画包裹在减少动画的判断逻辑中。若自定义了其他套件动画,也需进行类似处理。

Calls a11y

通话场景的无障碍优化

Calls have specific a11y considerations beyond chat:
  1. Focus on call screen entry → end-call button. Avoids accidental hangup but ensures the user can quickly exit.
  2. Announce incoming calls. Live region (web/RN) or
    UIAccessibility.post(.announcement)
    (iOS) — "Incoming call from Alice."
  3. Mute/end button labels. "Mute microphone" not just "Mute" — clarify what's being toggled.
  4. No flashing — recording indicator uses dot, not strobe. WCAG 2.3 forbids more than 3 flashes/sec to prevent seizures.
  5. Caption support if recording. Production calling apps with live captioning hook in via the platform's speech-recognition API and overlay text on the call screen.

通话场景的无障碍要求超出聊天场景:
  1. 进入通话屏幕时聚焦结束通话按钮。避免误挂断,同时确保用户可快速退出通话。
  2. 播报来电通知。使用实时区域(Web/React Native)或
    UIAccessibility.post(.announcement)
    (iOS)——“来自Alice的来电”。
  3. 静音/结束按钮的明确标签。使用“静音麦克风”而非仅“静音”——明确说明切换的对象。
  4. 避免闪烁——录制指示器使用圆点而非频闪。WCAG 2.3标准禁止每秒超过3次的闪烁,以防引发癫痫。
  5. 录制时支持字幕。带有实时字幕的生产级通话应用需对接平台语音识别API,并在通话屏幕上叠加文本。

Testing — automated + manual

测试——自动化+手动

Web automated

Web自动化测试

bash
npm install --save-dev @axe-core/playwright
ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("chat screen passes axe AA", async ({ page }) => {
  await page.goto("/messages");
  const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa"]).analyze();
  expect(results.violations).toEqual([]);
});
bash
npm install --save-dev @axe-core/playwright
ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("聊天屏幕通过axe AA检查", async ({ page }) => {
  await page.goto("/messages");
  const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa"]).analyze();
  expect(results.violations).toEqual([]);
});

Native automated

原生自动化测试

iOS: Xcode → Accessibility Inspector → Audit. Android: Accessibility Scanner app on a real device.
iOS:Xcode → Accessibility Inspector → Audit。Android:在真机上使用Accessibility Scanner应用。

Manual

手动测试

  • Keyboard-only navigation — unplug your mouse, complete a full chat flow. Tab through every control.
  • VoiceOver (iOS) / TalkBack (Android) / NVDA (Windows) / VoiceOver (macOS) — listen to the kit; verify announcements make sense.
  • Browser zoom 200% — kit should remain usable at 200% zoom on a 1280×720 viewport (WCAG 1.4.10 reflow).

  • 纯键盘导航——拔掉鼠标,完成完整聊天流程,按Tab键遍历所有控件。
  • VoiceOver(iOS)/ TalkBack(Android)/ NVDA(Windows)/ VoiceOver(macOS)——收听套件播报,验证播报内容清晰合理。
  • 浏览器缩放200%——在1280×720视口下,套件在200%缩放时仍需可用(符合WCAG 1.4.10重排要求)。

Anti-patterns

反模式

  1. Custom themes without a contrast audit. Brand colors silently fail AA.
  2. <div onClick>
    for clickable items.
    Keyboard users can't activate.
  3. No live region for new messages. Screen reader users miss messages.
  4. Auto-playing video on call screen. Some users browse with autoplay disabled — kit handles this; custom UI must too.
  5. Focus stays on the trigger button after opening chat. User has to manually re-navigate.
  6. Ignoring
    prefers-reduced-motion
    .
    Vestibular-disorder users get disoriented.
  7. aria-live="assertive"
    for chat messages.
    Interrupts the user mid-sentence; reserve for genuinely urgent (incoming call).
  8. Skipping label specificity. "Mute" alone is ambiguous; "Mute microphone" / "Unmute microphone" is clear.
  9. Color-only signals. "Read" status as just a checkmark color — add a visible text label for color-blind users.
  10. No test coverage for a11y. Regressions slip in. Automate the easy checks (axe-core); manual-test the rest per release.

  1. 自定义主题未进行对比度审计。品牌色悄悄不符合AA标准。
  2. 为可点击元素使用
    <div onClick>
    。键盘用户无法激活。
  3. 新消息无实时区域播报。屏幕阅读器用户错过消息。
  4. 通话屏幕自动播放视频。部分用户禁用自动播放——套件已处理此问题;自定义UI也需遵循。
  5. 打开聊天后焦点仍停留在触发按钮上。用户需手动重新导航。
  6. 忽略
    prefers-reduced-motion
    设置
    。前庭障碍用户会感到不适。
  7. 聊天消息使用
    aria-live="assertive"
    。打断用户操作;仅用于真正紧急的场景(如来电)。
  8. 标签不够明确。仅“静音”存在歧义;“静音麦克风”/“取消静音麦克风”更清晰。
  9. 仅用颜色传递信号。将“已读”状态仅设为对勾颜色——为色盲用户添加可见文本标签。
  10. 无障碍测试覆盖不足。回归问题悄然出现。自动化检查简单项(axe-core);每个版本手动测试其余项。

Verification checklist

验证清单

Cross-family:
  • Custom theme passes AA contrast (4.5:1 text, 3:1 large text + UI components)
  • Focus lands on a meaningful control on chat screen entry
  • Live region / accessibility announcement on new message receive
  • No
    <div onClick>
    patterns — buttons are buttons
  • prefers-reduced-motion
    /
    isReduceMotionEnabled
    honored for animations
  • Mute/end/camera labels are specific (not just "Mute")
  • Incoming-call announcement (
    assertive
    live region OR platform announcement API)
  • No flashing > 3 Hz (recording dot uses fade, not strobe)
Web/Angular:
  • axe-core / Playwright a11y test in CI; passes WCAG AA tags
  • <html lang="...">
    set to current locale (consumes
    cometchat-i18n
    skill output)
  • Skip-to-composer link present
  • Browser zoom 200% smoke test
Native (Android/iOS/Flutter):
  • TalkBack / VoiceOver smoke test on a real device
  • Reduced motion preference observed (
    UIAccessibility.isReduceMotionEnabled
    , etc.)
  • Focus restored on screen pop (not just push)
  • Touch targets ≥ 44×44 pt (iOS) / 48×48 dp (Android) — kit defaults pass; custom UI must too

跨框架通用:
  • 自定义主题通过AA对比度检查(文本4.5:1,大文本+UI组件3:1)
  • 进入聊天屏幕时焦点落在有意义的控件上
  • 收到新消息时有实时区域/无障碍播报
  • <div onClick>
    模式——按钮使用
    <button>
  • 动画遵循
    prefers-reduced-motion
    /
    isReduceMotionEnabled
    设置
  • 静音/结束/摄像头按钮标签明确(非仅“静音”)
  • 来电播报(
    assertive
    实时区域或平台播报API)
  • 无超过3Hz的闪烁(录制指示器使用淡入而非频闪)
Web/Angular:
  • CI中包含axe-core/Playwright无障碍测试,通过WCAG AA标签检查
  • <html lang="...">
    设置为当前语言环境(依赖
    cometchat-i18n
    指南输出)
  • 存在跳转到输入框的链接
  • 浏览器缩放200%冒烟测试
原生(Android/iOS/Flutter):
  • 在真机上进行TalkBack/VoiceOver冒烟测试
  • 遵循减少动画偏好设置(
    UIAccessibility.isReduceMotionEnabled
    等)
  • 返回屏幕时恢复焦点(不仅是进入屏幕)
  • 触摸目标≥44×44 pt(iOS)/48×48 dp(Android)——套件默认符合;自定义UI也需遵循

Pointers

参考链接