Theme System
RNCopilot uses a semantic token-based theme system powered by react-native-unistyles 3.x. Every color, spacing value, font size, and border radius is defined as a theme token — never hardcoded in component styles.
Color Palette
The theme is built around two primary colors:
| Role | Light Mode | Dark Mode | Usage |
|---|---|---|---|
| Primary | Indigo #6366F1 | Bright Indigo #818CF8 | Buttons, links, active states |
| Accent | Teal #14B8A6 | Bright Teal #2DD4BF | Highlights, badges, accent text |
| Secondary | Dark Slate #1E293B | Light Slate #CBD5E1 | Headings, secondary icons |
Light and Dark Modes
Both themes share the same token structure (ThemeColors), ensuring that any component styled with theme tokens automatically adapts to the active mode.
Light
// src/theme/light-theme.ts (excerpt)
export const lightColors: ThemeColors = {
mode: 'light',
brand: {
primary: '#6366F1', // Indigo
secondary: '#1E293B', // Dark Slate
tertiary: '#14B8A6', // Teal
primaryVariant: '#4F46E5',
secondaryVariant: '#334155',
},
background: {
app: '#F8FAFC',
surface: '#FFFFFF',
surfaceAlt: '#F1F5F9',
// ...
},
// ...
};Token Categories
The ThemeColors interface is organized into nine semantic categories:
brand — Brand Identity
theme.colors.brand.primary -- Main brand color (Indigo)
theme.colors.brand.secondary -- Secondary brand color (Slate)
theme.colors.brand.tertiary -- Accent color (Teal)
theme.colors.brand.primaryVariant -- Darker/lighter variant of primary
theme.colors.brand.secondaryVariant -- Darker/lighter variant of secondarybackground — Surface Colors
theme.colors.background.app -- Page/screen background
theme.colors.background.surface -- Cards, sheets, panels
theme.colors.background.surfaceAlt -- Alternate surface (e.g., striped rows)
theme.colors.background.section -- Section backgrounds
theme.colors.background.elevated -- Elevated elements (floating cards)
theme.colors.background.input -- Input field backgrounds
theme.colors.background.disabled -- Disabled element backgrounds
theme.colors.background.modal -- Modal/dialog backgroundstext — Typography
theme.colors.text.primary -- Headings, body text
theme.colors.text.secondary -- Supporting/subtitle text
theme.colors.text.tertiary -- Metadata, timestamps
theme.colors.text.muted -- Placeholders, hints
theme.colors.text.inverse -- Text on primary/dark backgrounds
theme.colors.text.accent -- Teal accent text
theme.colors.text.link -- Link text
theme.colors.text.linkHover -- Link hover stateborder — Borders and Dividers
theme.colors.border.default -- Standard borders
theme.colors.border.subtle -- Very light borders
theme.colors.border.strong -- Prominent borders
theme.colors.border.focus -- Input focus rings
theme.colors.border.disabled -- Disabled element bordersicon — Iconography
theme.colors.icon.primary -- Primary icons (brand color)
theme.colors.icon.secondary -- Secondary icons (dark/light)
theme.colors.icon.tertiary -- Subtle icons
theme.colors.icon.muted -- Very subtle icons
theme.colors.icon.inverse -- Icons on dark backgrounds
theme.colors.icon.accent -- Teal accent iconsstate — Semantic States
theme.colors.state.success -- Success foreground (#10B981)
theme.colors.state.successBg -- Success background (#ECFDF5)
theme.colors.state.warning -- Warning foreground (#F59E0B)
theme.colors.state.warningBg -- Warning background
theme.colors.state.error -- Error foreground (#EF4444)
theme.colors.state.errorBg -- Error background
theme.colors.state.info -- Info foreground (#3B82F6)
theme.colors.state.infoBg -- Info background
theme.colors.state.disabled -- Disabled foregroundoverlay — Overlays and Effects
theme.colors.overlay.modal -- Modal backdrop
theme.colors.overlay.pressed -- Press feedback
theme.colors.overlay.hover -- Hover feedback
theme.colors.overlay.focus -- Focus ring overlay
theme.colors.overlay.ripple -- Ripple effect
theme.colors.overlay.shadow -- Shadow colorgradient — Gradient Pairs
theme.colors.gradient.primary -- [start, end] primary gradient
theme.colors.gradient.secondary -- [start, end] secondary gradient
theme.colors.gradient.accent -- [start, end] teal accent gradient
theme.colors.gradient.success -- [start, end] green success gradient
theme.colors.gradient.highlight -- [start, end] purple highlight gradientshadow — Shadow Configuration
theme.colors.shadow.color -- Shadow color
theme.colors.shadow.elevation -- Default elevation
theme.colors.shadow.elevationSmall -- Small shadow
theme.colors.shadow.elevationMedium -- Medium shadow
theme.colors.shadow.elevationLarge -- Large shadowChecking the Current Mode
Use theme.colors.mode to conditionally render based on the active theme:
const styles = StyleSheet.create((theme) => ({
container: {
// theme.colors.mode is 'light' or 'dark'
borderWidth: theme.colors.mode === 'dark' ? 0 : 1,
},
}));In a component, access the current theme via useUnistyles:
import { useUnistyles } from 'react-native-unistyles';
function MyComponent() {
const { theme } = useUnistyles();
const isDark = theme.colors.mode === 'dark';
return (
<Icon
name={isDark ? 'moon' : 'sun'}
color={theme.colors.icon.primary}
/>
);
}Toggling Themes at Runtime
import { toggleDarkMode } from '@/theme/themeManager';
// Switch to dark mode (persisted to MMKV)
toggleDarkMode(true);
// Switch to light mode
toggleDarkMode(false);The preference is persisted to MMKV under STORAGE_KEYS.preferences.theme and restored on next launch.
Responsive Metrics
RNCopilot provides three scaling functions that adapt values to the device screen size. They use iPhone 14 (390 x 844) as the base reference:
| Function | Purpose | Base | Example |
|---|---|---|---|
rf(n) | Responsive font size | 390px width | rf(16) scales a 16pt font proportionally |
hs(n) | Horizontal scale | 390px width | hs(20) for horizontal padding/margins |
vs(n) | Vertical scale | 844px height | vs(24) for vertical padding/margins |
Using Metrics via Theme Tokens
The preferred way to use spacing is through the pre-computed theme tokens:
const styles = StyleSheet.create((theme) => ({
container: {
paddingHorizontal: theme.metrics.spacing.p16, // hs(16)
paddingVertical: theme.metrics.spacingV.p24, // vs(24)
fontSize: theme.fonts.size.md, // rf(16)
borderRadius: theme.metrics.borderRadius.md, // hs(8)
},
}));Available Spacing Tokens
theme.metrics.spacing.p4 ... p120 -- Horizontal spacing (hs-based)
theme.metrics.spacingV.p4 ... p120 -- Vertical spacing (vs-based)
theme.fonts.size.xxs = rf(10)
theme.fonts.size.xs = rf(12)
theme.fonts.size.sm = rf(14)
theme.fonts.size.md = rf(16)
theme.fonts.size.lg = rf(18)
theme.fonts.size.xl = rf(20)
theme.fonts.size.2xl = rf(24)
theme.fonts.size.3xl = rf(30)
theme.fonts.size.4xl = rf(36)
theme.metrics.borderRadius.xs = hs(4)
theme.metrics.borderRadius.sm = hs(6)
theme.metrics.borderRadius.md = hs(8)
theme.metrics.borderRadius.lg = hs(12)
theme.metrics.borderRadius.xl = hs(16)
theme.metrics.borderRadius.full = 999
theme.metrics.iconSize.xs = hs(14)
theme.metrics.iconSize.sm = hs(16)
theme.metrics.iconSize.md = hs(18)
theme.metrics.iconSize.lg = hs(20)
theme.metrics.iconSize.xl = hs(24)Using the Raw Functions
For one-off values not covered by the token system, import the helpers directly:
import { rf, hs, vs } from '@/theme/metrics';
const styles = StyleSheet.create((theme) => ({
customBanner: {
height: vs(200),
paddingHorizontal: hs(32),
fontSize: rf(22),
},
}));Prefer theme tokens (theme.metrics.spacing.p16) over raw function calls (hs(16)). Tokens are centralized and easier to update across the entire codebase.