Imported from cometchat/cometchat-skills (
skills/cometchat-native-theming/SKILL.md). Install upstream withnpx skills add cometchat/cometchat-skills --skill cometchat-native-theming. Copyright stays with the author (MIT).
Purpose
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).
1. How theming works (no CSS — JS theme object)
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 styles
The 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>
Style precedence (highest to lowest)
- Component
style={}prop — wins always. Per-component tweak, overrides everything. - Custom theme via
CometChatThemeProvider— app-wide. - Default theme — the UI Kit's built-in palette.
So for a one-off color on a single component, use style={}. For a brand-wide change (primary color everywhere), use the theme.
Deep merge
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
},
},
}}
2. The CometChatThemeProvider
Minimum setup — follow system light/dark
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
<CometChatThemeProvider>
{/* children read the current system mode automatically */}
</CometChatThemeProvider>
Force a mode
<CometChatThemeProvider theme={{ mode: "light" }}>{/* ... */}</CometChatThemeProvider>
<CometChatThemeProvider theme={{ mode: "dark" }}>{/* ... */}</CometChatThemeProvider>
App-controlled theme toggle (wire to the project's existing theme system)
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.
Placement in the wrapper chain
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.
3. Color tokens
Every color is a hex string ("#F76808" — never "rgb(...)" or named colors).
Primary (brand accent)
| 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. |
Neutrals (surfaces + borders)
| 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 |
Background aliases
| Token | Maps to (default) | Controls |
|---|---|---|
background1 |
neutral50 |
Main app background |
background2 |
neutral100 |
Sidebars, panels |
background3 |
neutral200 |
Nested panels, cards |
background4 |
neutral300 |
Additional surface |
Text
| Token | Default | Controls |
|---|---|---|
textPrimary |
neutral900 |
Main body text |
textSecondary |
neutral600 |
Timestamps, subtitles |
textTertiary |
neutral500 |
Hints, placeholders |
textHighlight |
primary |
Links, mentions |
Icon
| Token | Default | Controls |
|---|---|---|
iconPrimary |
neutral900 |
Active / default icons |
iconSecondary |
neutral500 |
Inactive icons |
iconHighlight |
primary |
Action icons |
Semantic (state indicators)
| Token | Default (light) | Controls |
|---|---|---|
info |
#0B7BEA |
Info callouts, links |
warning |
#FFAB00 |
Warning callouts |
success |
#09C26F |
Online indicator, success messages |
error |
#F44649 |
Error messages, validation |
Bubble-specific
| Token | Default | Controls |
|---|---|---|
sendBubbleBackground |
primary |
Outgoing bubble bg |
sendBubbleText |
staticWhite (#FFFFFF) |
Outgoing bubble text |
receiveBubbleBackground |
neutral300 |
Incoming bubble bg |
receiveBubbleText |
neutral900 |
Incoming bubble text |
Static (never flip light/dark)
| Token | Value | Controls |
|---|---|---|
staticBlack |
#141414 |
Fixed dark elements (overlays, opacity-based) |
staticWhite |
#FFFFFF |
Fixed light elements |
4. Mode: light / dark / system
Follow system
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>
Force a specific mode
<CometChatThemeProvider theme={{ mode: "dark" }}>{/* ... */}</CometChatThemeProvider>
Toggle controlled by your app
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.
Dark-mode palette
Override the dark branch of the theme for a custom dark palette:
<CometChatThemeProvider
theme={{
light: { color: { primary: "#6852D6" } },
dark: { color: { primary: "#A594F3", background1: "#0B0B0F" } },
}}
>
5. Typography overrides
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).
Custom font setup
React Native font loading is NOT covered by the UI Kit — use your project's existing font system:
- Expo:
useFonts()fromexpo-font, load before rendering the provider - Bare RN: add fonts to
ios/<App>/Info.plistUIAppFonts+android/app/src/main/assets/fonts/+ runnpx react-native-asset
Only reference a fontFamily in the theme once the font is actually loaded — otherwise iOS shows the system default and Android crashes.
6. Per-component style blocks
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).
Source of truth for available keys
The exact list of style keys per component is authoritative in the kit's type file:
packages/ChatUiKit/src/theme/type.ts
If 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).
7. Common recipes
Match a brand color (most common)
<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.
Dark mode + custom brand
<CometChatThemeProvider
theme={{
light: { color: { primary: "#FF6B35" } },
dark: { color: { primary: "#FF8F66", background1: "#1A1A1A" } },
}}
>
<App />
</CometChatThemeProvider>
Custom message-bubble colors
<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).
Custom font across the whole UI
- Load font (Expo
useFontsor barenpx react-native-asset) - Override the typography block:
<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" } },
},
},
}}
>
8. Reading the theme in custom views
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.
9. Localization — CometChatI18nProvider
Theming 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.
9a. Built-in locales
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-tw
9b. Wrapper chain with i18n — five wrappers, not four
CometChatI18nProvider 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>
9c. Auto-detect (default)
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 RN
9d. Force a specific language
Override 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.
9e. Fallback behavior
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.
9f. Custom translations — override or add languages
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.
9g. Reading the language inside custom views
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.
9h. Common pitfall — i18n outside the provider
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.
Sound Manager — custom notification & call sounds
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.
10. Anti-patterns
-
Don't pass non-hex colors.
"rgb(...)","rgba(...)", named colors, orhsl(...)will break the kit's internal color math (used to deriveextendedPrimary). Use"#RRGGBB"or"#RRGGBBAA"(opacity via alpha). -
Don't override
staticBlack/staticWhite. They're "static" for a reason — used in places where a specific absolute color is needed regardless of theme (overlays, badges on fixed-color avatars). Overriding them breaks visual consistency. -
Don't override extended primary colors unless you need to.
extendedPrimary50–900are auto-derived fromprimary. Override them only if the auto-derivation doesn't match your brand's tints — and then override the full range, not just one level. -
Don't wrap
CometChatThemeProviderinside a screen. It belongs at the app root, once. Re-wrapping per screen creates hydration-like flashes on navigation and breaks dark-mode switching. -
Don't mix theme overrides and per-component
style={}for the same property.style={}wins — the theme override becomes dead code. Pick one: theme for app-wide,style={}for one-offs. -
Don't reference an unloaded font in typography. iOS silently falls back to system default; Android crashes. Gate the provider on font loading:
// 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> ); -
Don't bypass the theme via
useColorScheme()in a custom view. CalluseTheme()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.
11. Verifying a theme change
After changing the theme:
- Hard-reload the Metro bundler (not Fast Refresh — theme context sometimes doesn't update on Fast Refresh)
- Send a message — check the outgoing bubble color matches
primary/sendBubbleBackground - Toggle dark mode on the device (iOS: Settings → Display; Android: Settings → Display → Dark theme)
- Check that both modes render without reloading the app
If something looks unstyled or crashes:
- Check the color is a hex string (not a name or rgb)
- Check the font (if you overrode typography) is actually loaded
- Check the override key matches the type in
packages/ChatUiKit/src/theme/type.ts
Skill routing reference
| 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 |