cometchat-native-theming-56ff4c — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-native-theming-56ff4c (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.
Teaches Claude how to theme and localize the React Native UI Kit via CometChatThemeProvider + CometChatI18nProvider. No CSS — React Native uses a JS theme object instead. This skill covers color tokens, typography, light/dark modes, per-component style overrides, the useTheme() hook for custom views, and localization (18 built-in languages, device auto-detect, custom translation overrides) via useCometChatTranslation().
Read `cometchat-native-core` first (the wrapper chain that includes CometChatThemeProvider) before this skill. cometchat-native-components § 13 covers per-component style={} overrides, which are a sibling concern to theming.
Ground truth: docs/ui-kit/react-native/theme.mdx, colors.mdx, component-styling.mdx, message-bubble-styling.mdx, and packages/ChatUiKit/src/theme/type.ts (the canonical type definitions). Official docs: https://www.cometchat.com/docs/ui-kit/react-native/overview · Docs MCP: claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp (or fetch the URL directly without MCP).
React Native has no CSS. Instead:
CometChatThemeProvider
↓ (provides theme via React Context)
every <CometChat*> component reads theme via internal useTheme()
↓
component's default styles merge with theme overrides → rendered stylesThe theme object you pass has two top-level keys for light/dark variants:
<CometChatThemeProvider
theme={{
mode: "light", // or "dark", or omit for OS-default
light: { color: { primary: "#F76808" } },
dark: { color: { primary: "#FF8A3D" } },
}}
>
<App />
</CometChatThemeProvider>CometChatThemeProvider — app-wide.So for a one-off color on a single component, use style={}. For a brand-wide change (primary color everywhere), use the theme.
Theme values are deeply merged with defaults — you only specify what you want to change:
theme={{
light: {
color: {
primary: "#F76808", // override just primary; everything else keeps defaults
},
typography: {
heading1: { fontWeight: "700" }, // override just heading1 weight
},
},
}}import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
<CometChatThemeProvider>
{/* children read the current system mode automatically */}
</CometChatThemeProvider><CometChatThemeProvider theme={{ mode: "light" }}>{/* ... */}</CometChatThemeProvider>
<CometChatThemeProvider theme={{ mode: "dark" }}>{/* ... */}</CometChatThemeProvider>If the project already has a dark-mode toggle, wire CometChatThemeProvider's mode prop to the same source. RN doesn't have CSS selectors — the React Native theme is just a value held somewhere in JS, and you forward that value to CometChat. Three common shapes:
// Pattern A — OS-driven (no toggle yet, just react to system)
import { useColorScheme } from "react-native";
function ThemedRoot({ children }: { children: React.ReactNode }) {
const scheme = useColorScheme(); // "light" | "dark" | null
return (
<CometChatThemeProvider theme={{ mode: scheme === "dark" ? "dark" : "light" }}>
{children}
</CometChatThemeProvider>
);
}// Pattern B — App-controlled toggle via custom Context
const ThemeContext = createContext<{ mode: "light" | "dark"; toggle: () => void }>({
mode: "light",
toggle: () => {},
});
function ThemedRoot({ children }: { children: React.ReactNode }) {
const { mode } = useContext(ThemeContext);
return (
<CometChatThemeProvider theme={{ mode }}>{children}</CometChatThemeProvider>
);
}// Pattern C — react-native-paper (or any provider that exposes a theme)
import { useTheme } from "react-native-paper";
function ThemedRoot({ children }: { children: React.ReactNode }) {
const paperTheme = useTheme();
return (
<CometChatThemeProvider theme={{ mode: paperTheme.dark ? "dark" : "light" }}>
{children}
</CometChatThemeProvider>
);
}How to tell which pattern the project uses:
| Library / setup | Where the mode lives | Wire theme={{ mode: ... }} to |
|---|---|---|
| Plain RN, no toggle yet | useColorScheme() from react-native | scheme === "dark" ? "dark" : "light" |
| Custom React Context | useContext(ThemeContext).mode (or whatever shape it has) | Read from the context |
react-native-paper | useTheme().dark | dark ? "dark" : "light" |
restyle | useTheme<Theme>().colors (no built-in mode flag — track separately) | A sibling Context that holds the mode string |
tamagui | useThemeName() returns the current theme name | name === "dark" ? "dark" : "light" |
Appearance.addChangeListener (manual OS) | A useState that mirrors Appearance.getColorScheme() | Read from that state |
Rule: wherever the project's theme toggle writes its current state, read from THAT and forward to CometChatThemeProvider's mode prop. Don't keep two parallel sources of truth.
Don't combine Pattern A with Pattern B unless the user explicitly wants "follow OS until the user opens settings and overrides." That hybrid is legitimate but usually over-engineered for a first integration — ship Pattern B alone if the project has a toggle, Pattern A if it doesn't.
CometChatThemeProvider is one of the four required wrappers — goes right above CometChatProvider, below SafeAreaProvider (see cometchat-native-core § 3):
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider theme={/* your theme */}>
<CometChatProvider appId={...} region={...} authKey={...}>
<YourApp />
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>Without CometChatThemeProvider, components throw or fall back to minimal styles. Even if you don't customize anything, the wrapper is mandatory.
Every color is a hex string ("#F76808" — never "rgb(...)" or named colors).
| Token | Controls |
|---|---|
primary | Outgoing message bubbles, send button, active tabs, buttons |
extendedPrimary50–900 | Auto-derived shades of primary. Used for hover, pressed, subtle accents. Only override these if you need finer control — the auto-derivation is usually correct. |
| Token | Default (light) | Controls |
|---|---|---|
neutral50 | #FFFFFF | White/light surface, background1 default |
neutral100 | #FAFAFA | background2 default |
neutral200 | #F5F5F5 | background3 default |
neutral300 | #E8E8E8 | Incoming bubble default, borders |
neutral400 | #DCDCDC | Divider lines |
neutral500 | #A1A1A1 | Placeholder / muted text, iconSecondary default |
neutral600 | #727272 | textSecondary (timestamps, subtitles) |
neutral700 | #5B5B5B | Body text tier 3 |
neutral800 | #434343 | Headings default |
neutral900 | #141414 | textPrimary default, iconPrimary default |
| Token | Maps to (default) | Controls |
|---|---|---|
background1 | neutral50 | Main app background |
background2 | neutral100 | Sidebars, panels |
background3 | neutral200 | Nested panels, cards |
background4 | neutral300 | Additional surface |
| Token | Default | Controls |
|---|---|---|
textPrimary | neutral900 | Main body text |
textSecondary | neutral600 | Timestamps, subtitles |
textTertiary | neutral500 | Hints, placeholders |
textHighlight | primary | Links, mentions |
| Token | Default | Controls |
|---|---|---|
iconPrimary | neutral900 | Active / default icons |
iconSecondary | neutral500 | Inactive icons |
iconHighlight | primary | Action icons |
| Token | Default (light) | Controls |
|---|---|---|
info | #0B7BEA | Info callouts, links |
warning | #FFAB00 | Warning callouts |
success | #09C26F | Online indicator, success messages |
error | #F44649 | Error messages, validation |
| Token | Default | Controls |
|---|---|---|
sendBubbleBackground | primary | Outgoing bubble bg |
sendBubbleText | staticWhite (#FFFFFF) | Outgoing bubble text |
receiveBubbleBackground | neutral300 | Incoming bubble bg |
receiveBubbleText | neutral900 | Incoming bubble text |
| Token | Value | Controls |
|---|---|---|
staticBlack | #141414 | Fixed dark elements (overlays, opacity-based) |
staticWhite | #FFFFFF | Fixed light elements |
Don't pass mode — the provider reads the OS setting via useColorScheme() and re-renders on change. The user gets automatic dark mode when they flip the system setting.
<CometChatThemeProvider>{/* ... */}</CometChatThemeProvider><CometChatThemeProvider theme={{ mode: "dark" }}>{/* ... */}</CometChatThemeProvider>If your app has its own dark-mode switch (stored in user prefs or Redux), drive mode from that state:
const [darkMode, setDarkMode] = useState(false);
// ...
<CometChatThemeProvider theme={{ mode: darkMode ? "dark" : "light" }}>
<Switch value={darkMode} onValueChange={setDarkMode} />
<App />
</CometChatThemeProvider>The provider re-renders children and they pick up the new theme immediately.
Override the dark branch of the theme for a custom dark palette:
<CometChatThemeProvider
theme={{
light: { color: { primary: "#6852D6" } },
dark: { color: { primary: "#A594F3", background1: "#0B0B0F" } },
}}
>The theme has a typography block with tokens per role:
<CometChatThemeProvider
theme={{
light: {
typography: {
fontFamily: "Inter", // flat string — the global family
// Every role EXCEPT `fontFamily`/`link` is a variant object with
// `bold` / `medium` / `regular`, each an RN TextStyle:
heading1: {
bold: { fontFamily: "Inter-Bold", fontSize: 28, fontWeight: "700" },
medium: { fontFamily: "Inter-SemiBold", fontSize: 28, fontWeight: "600" },
regular: { fontFamily: "Inter-Regular", fontSize: 28, fontWeight: "400" },
},
body: {
regular: { fontFamily: "Inter-Regular", fontSize: 15 },
},
link: { fontFamily: "Inter-Regular", fontSize: 15 }, // `link` is a flat TextStyle
// ... etc
},
},
}}
>Common tokens: title, heading1, heading2, heading3, heading4, body, caption1, caption2, button (there is NO body1/body2/button1/button2). Each of these is a variant object { bold, medium, regular } where each weight is an RN TextStyle (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing). The two exceptions — fontFamily and link — are flat (a string and a single TextStyle respectively).
React Native font loading is NOT covered by the UI Kit — use your project's existing font system:
useFonts() from expo-font, load before rendering the providerios/<App>/Info.plist UIAppFonts + android/app/src/main/assets/fonts/ + run npx react-native-assetOnly reference a fontFamily in the theme once the font is actually loaded — otherwise iOS shows the system default and Android crashes.
Beyond color / typography, the theme has per-component style blocks for fine control. These sit inside the light / dark branches:
<CometChatThemeProvider
theme={{
light: {
// component-specific overrides
conversationStyles: {
containerStyle: { backgroundColor: "#FAFAFA" },
},
messageHeaderStyles: {
titleTextStyle: { fontSize: 18 }, // key is titleTextStyle, NOT titleStyle
},
messageListStyles: {
containerStyle: { padding: 8 },
// Bubble styling is NESTED — the keys are `incomingMessageBubbleStyles`
// and `outgoingMessageBubbleStyles` (each a DeepPartial<BubbleStyles>
// with `containerStyle`/`textBubbleStyles`/… — there is no flat
// `sendBubbleStyle`/`receiveBubbleStyle` with backgroundColor+textStyle).
// For simple bubble color changes, prefer the color tokens in §7
// (sendBubbleBackground / receiveBubbleBackground) — cleaner than the
// per-component bubble blocks.
},
messageComposerStyles: {
containerStyle: { backgroundColor: "#FFF", borderTopWidth: 1, borderTopColor: "#E8E8E8" },
},
},
}}
>Common component-style keys (exact names from theme/type.ts): conversationStyles, userStyles, groupStyles, groupMemberStyle (singular "Member" + singular "Style"), messageHeaderStyles, messageListStyles, messageComposerStyles, threadHeaderStyles, callButtonStyles (no "s" after "Button"), callLogsStyles. Note the irregular pluralization — userStyles/groupStyles are singular-noun, and groupMemberStyle/callButtonStyles don't follow the *Styles pattern.
Each block has the same nested shape as the component's style prop (see cometchat-native-components § 13).
The exact list of style keys per component is authoritative in the kit's type file:
packages/ChatUiKit/src/theme/type.tsIf you're overriding a component style and the TypeScript compiler complains about an unknown key, check that file (or use useTheme() + autocomplete in your IDE).
<CometChatThemeProvider
theme={{ light: { color: { primary: "#FF6B35" } } }}
>
<App />
</CometChatThemeProvider>This single line changes the outgoing message bubble color, send button color, active tab indicator, and every primary accent in the UI. The extendedPrimary50–900 tints are auto-derived from primary.
<CometChatThemeProvider
theme={{
light: { color: { primary: "#FF6B35" } },
dark: { color: { primary: "#FF8F66", background1: "#1A1A1A" } },
}}
>
<App />
</CometChatThemeProvider><CometChatThemeProvider
theme={{
light: {
color: {
sendBubbleBackground: "#FF6B35",
sendBubbleText: "#FFFFFF",
receiveBubbleBackground: "#F0F0F0",
receiveBubbleText: "#1A1A1A",
},
},
}}
>Overriding the bubble tokens directly is cleaner than doing it via messageListStyles.sendBubbleStyle — the tokens apply consistently everywhere bubbles render (main list + thread panel + search results).
useFonts or bare npx react-native-asset)<CometChatThemeProvider
theme={{
light: {
// Roles: title, heading1-4, body, caption1, caption2, button (NO body1/body2/button1/button2).
// Each role is a VARIANT object { bold, medium, regular } — not a flat { fontFamily }.
// Verified vs uikit-react-native-v5 theme/default/typography.ts.
typography: {
heading1: { bold: { fontFamily: "Inter-Bold" } },
heading2: { bold: { fontFamily: "Inter-SemiBold" } },
heading3: { medium: { fontFamily: "Inter-SemiBold" } },
heading4: { medium: { fontFamily: "Inter-Medium" } },
body: { regular: { fontFamily: "Inter-Regular" } },
caption1: { regular: { fontFamily: "Inter-Regular" } },
caption2: { regular: { fontFamily: "Inter-Regular" } },
button: { medium: { fontFamily: "Inter-Medium" } },
},
},
}}
>When you write a custom slot view (e.g. a TitleView on CometChatMessageHeader) and want your custom component to match the theme, use the useTheme() hook:
import { useTheme } from "@cometchat/chat-uikit-react-native";
function CustomTitle({ user }: any) {
const theme = useTheme();
return (
<Text style={{
color: theme.color.textPrimary,
// heading3 is a variant object — read a weight (regular/medium/bold):
fontFamily: theme.typography.heading3.regular.fontFamily,
fontSize: theme.typography.heading3.regular.fontSize,
}}>
{user.getName()}
</Text>
);
}
// Wire into a header:
<CometChatMessageHeader user={selectedUser} TitleView={(user) => <CustomTitle user={user} />} />This is how you write custom views that automatically follow dark mode — by reading tokens from useTheme() instead of hardcoding colors.
CometChatI18nProviderTheming and localization are separate concerns but ship together. If your users aren't all English-speaking, wire CometChatI18nProvider alongside CometChatThemeProvider. Every string rendered by the UI Kit (message-action labels, empty states, system messages, alerts) flows through the i18n layer.
The UI Kit ships translations for 18 languages out of the box:
de, en, es, fr, hi, hu, it, ja, ko, lt, ms, nl, pt, ru, sv, tr, zh, zh-twCometChatI18nProvider goes above CometChatThemeProvider (theme is a child of i18n, not the other way around):
import { CometChatI18nProvider, CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatI18nProvider>
<CometChatThemeProvider>
<CometChatProvider>
<YourApp />
</CometChatProvider>
</CometChatThemeProvider>
</CometChatI18nProvider>
</SafeAreaProvider>
</GestureHandlerRootView>With no props, CometChatI18nProvider reads the device locale via react-native-localize and picks the matching language if available, falling back to English. No setup needed beyond the wrapper.
Install `react-native-localize` (required peer dep for auto-detect):
npx expo install react-native-localize # Expo managed
# or
npm install react-native-localize && cd ios && pod install && cd .. # bare RNOverride the device default via selectedLanguage:
<CometChatI18nProvider selectedLanguage="ja">If the user's app has its own language preference (stored in settings / Redux / MMKV), drive selectedLanguage from that state. The provider re-renders children on change so strings update immediately.
Chain: `selectedLanguage` (if set + available) → device language (if `autoDetectLanguage=true`) → `fallbackLanguage` (default `'en'`).
<CometChatI18nProvider
selectedLanguage={user.preferredLanguage} // from your app state
autoDetectLanguage={true} // fall back to device language
fallbackLanguage="en" // final fallback
>If the user's preferred language isn't bundled AND there's no custom translation for it, the provider logs a warning and uses the fallback.
Pass a translations object to override specific keys in an existing locale, or add a brand-new locale the UI Kit doesn't ship:
const translations = {
en: {
// Override a built-in English string
"NO_MESSAGES_YET": "Say hello to start the conversation!",
"SENT": "Delivered",
},
th: {
// Add a new language — Thai
"NO_MESSAGES_YET": "ยังไม่มีข้อความ",
"SENT": "ส่งแล้ว",
// ...provide the full key set
},
};
<CometChatI18nProvider selectedLanguage="th" translations={translations}>The translation schema is a flat { KEY: "string" } map. Keys are screaming-snake-case (NO_MESSAGES_YET, MESSAGE_COMPOSER_MENTION_ALL, TRANSLATE, etc.). Full key list lives at packages/ChatUiKit/src/shared/resources/CometChatLocalizeNew/resources/en/translation.json in the UI Kit source — grep for "KEY": there to find the exact key for a string you want to override.
When you write a custom slot view and need the current language (or want to translate your own strings using the same key set), use the useCometChatTranslation hook:
import { useCometChatTranslation } from "@cometchat/chat-uikit-react-native";
function CustomEmptyState() {
const { t, language } = useCometChatTranslation();
return <Text>{t("NO_MESSAGES_YET")}</Text>;
}The hook also exposes availableLanguages — useful for building a language-picker UI.
Calling useCometChatTranslation() from a component rendered OUTSIDE CometChatI18nProvider (common when a custom view mounts at the navigator root instead of inside the chat subtree) logs "useCometChatTranslation used outside provider, using fallback translations" and falls through to English. Check your wrapper chain — i18n must wrap every component that reads translations, which is the whole app tree in practice.
Sounds are a behavioral customization (not styling) — driven by CometChatSoundManager, exported from the kit. The UI Kit plays the built-in cues automatically; use this to override or trigger them. Sound-event keys are passed as strings. (Docs: ui-kit/react-native/sound-manager.)
import { CometChatSoundManager } from "@cometchat/chat-uikit-react-native";
// Play a default cue — keys: incomingMessage | incomingMessageFromOther
// | outgoingMessage | incomingCall | outgoingCall
CometChatSoundManager.play("incomingMessage");
// Stop whatever is playing
CometChatSoundManager.pause();RN audio assets differ from web URLs — for custom sounds, follow the per-platform asset guidance in ui-kit/react-native/sound-manager rather than passing a web URL.
"rgb(...)", "rgba(...)", named colors, or hsl(...) will break the kit's internal color math (used to derive extendedPrimary). Use "#RRGGBB" or "#RRGGBBAA" (opacity via alpha).extendedPrimary50–900 are auto-derived from primary. Override them only if the auto-derivation doesn't match your brand's tints — and then override the full range, not just one level.style={} wins — the theme override becomes dead code. Pick one: theme for app-wide, style={} for one-offs. // Expo example
const [fontsLoaded] = useFonts({ "Inter-Bold": require("./assets/Inter-Bold.ttf") });
if (!fontsLoaded) return null;
return (
<CometChatThemeProvider theme={{ light: { typography: { heading1: { fontFamily: "Inter-Bold" } } } }}>
<App />
</CometChatThemeProvider>
);useTheme() from @cometchat/chat-uikit-react-native — that gives you the current theme (including any overrides you set). useColorScheme() only gives you the raw system mode.After changing the theme:
primary / sendBubbleBackgroundIf something looks unstyled or crashes:
packages/ChatUiKit/src/theme/type.ts| Skill | When to route |
|---|---|
cometchat-native-core | Always read first — init/login/provider wrapper chain |
cometchat-native-components | For per-component style={} prop (sibling concern to theming) |
cometchat-native-placement | Where CometChat components go |
cometchat-native-theming | This skill — app-wide color/typography/dark mode |
cometchat-native-customization | Custom slot views + useTheme() in your own components |
cometchat-native-expo-patterns | Expo font loading via expo-font |
cometchat-native-bare-patterns | Bare RN font loading via react-native-asset |
cometchat-native-troubleshooting | Colors not applying, dark mode not switching, font shows system default |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.