cometchat-native-theming
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).
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: {
heading1: { fontFamily: "Inter-Bold", fontSize: 28, fontWeight: "700" },
heading2: { fontFamily: "Inter-SemiBold", fontSize: 20 },
body1: { fontFamily: "Inter-Regular", fontSize: 15 },
caption1: { fontFamily: "Inter-Regular", fontSize: 12 },
// ... etc
},
},
}}
>
Common tokens: heading1, heading2, heading3, heading4, body1, body2, caption1, caption2, button1, button2. Each follows the RN TextStyle shape — fontFamily, fontSize, fontWeight, lineHeight, letterSpacing.
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: {
titleStyle: { fontSize: 18 },
},
messageListStyles: {
containerStyle: { padding: 8 },
sendBubbleStyle: {
backgroundColor: "#F76808",
textStyle: { color: "#FFFFFF" },
},
receiveBubbleStyle: {
backgroundColor: "#F5F5F5",
textStyle: { color: "#141414" },
},
},
messageComposerStyles: {
containerStyle: { backgroundColor: "#FFF", borderTopWidth: 1, borderTopColor: "#E8E8E8" },
},
},
}}
>
Common component-style keys: conversationStyles, usersStyles, groupsStyles, groupMembersStyles, messageHeaderStyles, messageListStyles, messageComposerStyles, threadHeaderStyles, callButtonsStyles, callLogsStyles.
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: {
typography: {
heading1: { fontFamily: "Inter-Bold" },
heading2: { fontFamily: "Inter-SemiBold" },
heading3: { fontFamily: "Inter-SemiBold" },
heading4: { fontFamily: "Inter-Medium" },
body1: { fontFamily: "Inter-Regular" },
body2: { fontFamily: "Inter-Regular" },
caption1: { fontFamily: "Inter-Regular" },
caption2: { fontFamily: "Inter-Regular" },
button1: { fontFamily: "Inter-SemiBold" },
button2: { 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,
fontFamily: theme.typography.heading3.fontFamily,
fontSize: theme.typography.heading3.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.
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 |