cometchat-a11y — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-a11y (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Ground truth: per-platform UI Kit +docs/fundamentals. Official docs: https://www.cometchat.com/docs/fundamentals/overview · Docs MCP:claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp(or fetch the URL directly without MCP). Verify symbols against the installed package/source before relying on them.
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.
<div> instead of <button> skip keyboard events.prefers-reduced-motion.This skill addresses each one across families.
CometChat themes are CSS variables (web/RN) or color tokens (native/Flutter). Override a single color and you might fail AA.
// 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.
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.
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.
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 is a plain function component (no forwardRef in v6), so passing it a ref is a no-op. Wrap it in a focusable container and reach the input through the DOM: composerRef.current?.querySelector("input, [contenteditable]")?.focus().
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>
);
}@Component({...})
export class ChatComponent implements AfterViewInit {
@ViewChild("composer") composer!: ElementRef;
ngAfterViewInit() {
setTimeout(() => this.composer.nativeElement.focus(), 100);
}
}override fun onResume() {
super.onResume()
composerView.requestFocus()
composerView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibility.post(notification: .screenChanged, argument: composerView)
}final FocusNode _composerFocus = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_composerFocus.requestFocus();
});
}
// Then on the composer widget: focusNode: _composerFocusScreen reader users need an audible announcement when a new message arrives — otherwise they have to navigate to the message list and re-read it.
<!-- 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>// 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.
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);
},
}));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.
The kit's components are keyboard-accessible by default. Custom wrapping is what breaks it.
<div onClick> for clickable items// ✗ 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>For long conversation lists, add a skip-to-message-composer link:
<a href="#message-composer" class="skip-link">Skip to message composer</a>.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.
For productivity apps:
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.
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.
@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;
}
}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 }],
}}
/>let reduceMotion = UIAccessibility.isReduceMotionEnabled
if !reduceMotion {
UIView.animate(withDuration: 0.3) { ... }
} else {
// Apply final state without animation
}val reduceMotion = Settings.Global.getFloat(
contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
) == 0.0ffinal 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.
Calls have specific a11y considerations beyond chat:
UIAccessibility.post(.announcement) (iOS) — "Incoming call from Alice."npm install --save-dev @axe-core/playwrightimport { 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([]);
});iOS: Xcode → Accessibility Inspector → Audit. Android: Accessibility Scanner app on a real device.
Cross-family:
<div onClick> patterns — buttons are buttonsprefers-reduced-motion / isReduceMotionEnabled honored for animationsassertive live region OR platform announcement API)Web/Angular:
<html lang="..."> set to current locale (consumes cometchat-i18n skill output)Native (Android/iOS/Flutter):
UIAccessibility.isReduceMotionEnabled, etc.)cometchat-i18n — sister skill; <html lang> and screen-reader pronunciation of translated strings depend on locale set correctlycometchat-{family}-customization — when customizing kit components, preserve their built-in a11y attributescometchat-{family}-troubleshooting — when a11y tools report issues that look kit-internal~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.