CometChat Flutter UIKit — Theming & Styling
How to customize the visual appearance of all CometChat components.
Theme System Architecture
Three layers, resolved via Flutter's ThemeExtension system:
CometChatColorPalette— all colors (primary, neutral, alert, background, text, icon, button, border)CometChatSpacing— spacing tokensCometChatTypography— text styles (heading1-4, body, caption1-2, button, link, title)
Access via static helpers:
final colors = CometChatThemeHelper.getColorPalette(context);
final spacing = CometChatThemeHelper.getSpacing(context);
final typography = CometChatThemeHelper.getTypography(context);
Applying a Custom Theme
Register CometChatColorPalette as a ThemeExtension on your ThemeData:
MaterialApp(
theme: ThemeData(
brightness: Brightness.light,
extensions: [
CometChatColorPalette(
primary: const Color(0xFF6852D6),
background1: Colors.white,
textPrimary: const Color(0xFF141414),
// ... override only what you need, rest falls back to defaults
),
],
),
darkTheme: ThemeData(
brightness: Brightness.dark,
extensions: [
CometChatColorPalette(
primary: const Color(0xFF604CC3),
background1: const Color(0xFF141414),
textPrimary: Colors.white,
),
],
),
)
Note: CometChatColorPalette does NOT have a const constructor (it has mutable default fields for white, black, transparent). Don't try const CometChatColorPalette(...) — it won't compile.
Dark Mode
CometChatThemeMode controls how brightness is resolved:
// Follow system setting (default)
CometChatThemeMode.mode = ThemeMode.system;
// Force light
CometChatThemeMode.mode = ThemeMode.light;
// Force dark
CometChatThemeMode.mode = ThemeMode.dark;
The helper reads brightness via:
// ThemeMode.system → MediaQuery.of(context).platformBrightness
// ThemeMode.light → Brightness.light
// ThemeMode.dark → Brightness.dark
Color Palette Tokens
| Category | Tokens | Default Source |
|---|---|---|
| Primary | primary |
#6852D6 light / #604CC3 dark |
| Extended Primary | extendedPrimary50–900 |
Auto-generated from primary via blend |
| Neutral | neutral50–900 |
10-shade grayscale |
| Alert | info, warning, error, success, error100 |
Semantic colors |
| Background | background1–4 |
Mapped from neutral shades |
| Text | textPrimary, textSecondary, textTertiary, textDisabled, textWhite, textHighlight |
Mapped from neutral/primary |
| Icon | iconPrimary, iconSecondary, iconTertiary, iconWhite, iconHighlight |
Mapped from neutral/primary |
| Button | buttonBackground, secondaryButtonBackground, buttonText, buttonIconColor, secondaryButtonText, secondaryButtonIcon |
Primary + neutral |
| Border | borderLight, borderDefault, borderDark, borderHighlight |
Neutral shades + primary |
| Special | white, black, messageSeen |
Fixed values |
Component Style Classes
Every component has a CometChat{Component}Style class with a merge() method:
CometChatConversations(
conversationsStyle: CometChatConversationsStyle(
backgroundColor: colors.background1,
titleStyle: typography.heading3?.bold,
),
)
CometChatMessageList(
style: CometChatMessageListStyle(
backgroundColor: colors.background3,
),
)
Style classes support merge() for combining defaults with overrides:
final baseStyle = CometChatConversationsStyle(backgroundColor: Colors.white);
final override = CometChatConversationsStyle(titleStyle: myTitleStyle);
final merged = baseStyle.merge(override); // backgroundColor + titleStyle
Theme Caching (Performance-Critical)
Cache theme in didChangeDependencies() — never in build():
class _MyWidgetState extends State<MyWidget> {
late CometChatColorPalette _colorPalette;
bool _themeInitialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_themeInitialized) {
_colorPalette = CometChatThemeHelper.getColorPalette(context);
_themeInitialized = true;
}
}
@override
Widget build(BuildContext context) {
return Container(color: _colorPalette.primary); // Cached, no lookup
}
}
For child widgets that receive theme from parent (hybrid pattern):
class CometChatImageBubble extends StatefulWidget {
final CometChatColorPalette? colorPalette; // Optional — parent can pass cached value
// ...
}
class _CometChatImageBubbleState extends State<CometChatImageBubble> {
late CometChatColorPalette colorPalette;
bool _themeInitialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_themeInitialized) {
colorPalette = widget.colorPalette ?? CometChatThemeHelper.getColorPalette(context);
_themeInitialized = true;
}
}
@override
void didUpdateWidget(CometChatImageBubble oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.colorPalette != oldWidget.colorPalette && widget.colorPalette != null) {
colorPalette = widget.colorPalette!;
}
}
}
Gotchas
CometChatColorPaletteis NOT const-constructible. It has mutable default fields (white = Colors.white,black = Colors.black,transparent = Colors.transparent). Writingconst CometChatColorPalette(...)orconst [CometChatColorPalette()]won't compile.CometChatThemeHelper.getColorPalette(context)doesTheme.of(context).extension<CometChatColorPalette>()internally — this is an InheritedWidget lookup. Inbuild()during keyboard animation, this fires every frame causing 44-95ms build times.- Extended primary colors are auto-generated by blending
primarywith white (light) or black (dark). Override individual shades only if the auto-blend doesn't match your brand. CometChatThemeMode.modeis a static field — changing it doesn't trigger rebuilds. You need to also changeThemeDatabrightness or callsetStateon theMaterialApp.- Style
merge()is null-aware: only non-null fields from the override replace the base. This means you can't explicitly set a field tonullvia merge.
Anti-Patterns
// ❌ WRONG — hardcoded colors
Container(color: Colors.purple)
// ✅ CORRECT — use theme tokens
Container(color: colorPalette.primary)
// ❌ WRONG — theme lookup in build
@override
Widget build(BuildContext context) {
final colors = CometChatThemeHelper.getColorPalette(context);
return Text('Hi', style: TextStyle(color: colors.textPrimary));
}
// ❌ ALSO WRONG — theme lookup in a method called from build
Widget _buildProfileMenu() {
final typography = CometChatThemeHelper.getTypography(context); // Still in build tree!
final spacing = CometChatThemeHelper.getSpacing(context);
// ...
}
// ✅ CORRECT — cached in didChangeDependencies
late CometChatColorPalette _colors;
late CometChatTypography _typography;
late CometChatSpacing _spacing;
bool _init = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_init) {
_colors = CometChatThemeHelper.getColorPalette(context);
_typography = CometChatThemeHelper.getTypography(context);
_spacing = CometChatThemeHelper.getSpacing(context);
_init = true;
}
}
@override
Widget build(BuildContext context) {
return Text('Hi', style: TextStyle(color: _colors.textPrimary));
}
// ❌ WRONG — MediaQuery.of(context).size triggers full rebuild
final size = MediaQuery.of(context).size;
// ✅ CORRECT — only subscribes to size changes
final size = MediaQuery.sizeOf(context);
Checklist
- Custom colors registered as
ThemeExtensiononThemeData - Both
themeanddarkThemeconfigured if supporting dark mode - Theme values cached in
didChangeDependencies(), notbuild() - No
CometChatThemeHelper.get*()calls in any method invoked duringbuild()(including helper methods like_buildProfileMenu()) -
_themeInitializedflag prevents re-init during keyboard animation - No hardcoded colors — all from
CometChatColorPalette -
MediaQuery.sizeOf(context)used instead ofMediaQuery.of(context).size - Style overrides use component's Style class, not inline styles