Theme System
Configure app appearance mode (light/dark/system) and theme family
Theme System
The app theme is built on two independent dimensions: appearance mode (light / dark / system) and theme family. Combined, they produce an active theme name that drives style rendering via Uniwind.
Two Dimensions
Appearance Mode (ThemeModePreference)
Controls whether the app uses a light or dark color scheme:
| Value | Description |
|---|---|
system | Follow the device system setting (default) |
light | Force light mode |
dark | Force dark mode |
Theme Family (ThemeFamily)
Controls the overall color tone. Four families are built in:
| Value | Style |
|---|---|
alpha | Default family |
lavender | Soft purple |
mint | Fresh green |
sky | Clear blue |
Active Theme Name
The two dimensions combine into the active theme name: {themeFamily}-{resolvedThemeMode}
For example, selecting lavender family + dark mode produces lavender-dark. This name is passed to Uniwind.setTheme() to switch component styles.
Key Files
| File | Description |
|---|---|
apps/native/providers/theme-provider.tsx | Theme state, persistence, and Uniwind sync |
apps/native/configs/app-config.ts | Storage key names |
User Preference Storage
Theme choices are persisted to device storage via AsyncStorage:
Storage key (from app-config.ts) | Content |
|---|---|
{AppName}_theme_preference | Appearance mode: system / light / dark |
{AppName}_theme_family | Theme family: alpha / lavender / mint / sky |
AppName comes from the app name configured in packages/app-config.
Changing the Default Theme
When ThemeProvider initializes and AsyncStorage has no stored values, it uses the defaults defined in useState:
const [themeModePreference, setThemeModePreferenceState] =
useState<ThemeModePreference>("system"); // default: follow system
const [themeFamily, setThemeFamilyState] = useState<ThemeFamily>("alpha"); // default: alphaChange the initial values to set a different out-of-box default for new users.
Adding a New Theme Family
Add the new family to the type
Edit theme-provider.tsx to extend THEME_FAMILIES and ThemeFamily:
const THEME_FAMILIES = ["alpha", "lavender", "mint", "sky", "ocean"] as const;
// ↑ new
export type ThemeFamily = "alpha" | "lavender" | "mint" | "sky" | "ocean";Register themes in Uniwind
Follow the same pattern as the existing alpha, lavender, and other families: create a CSS file, import it in global.css, then register it in metro.config.js.
1. Create apps/native/themes/ocean.css
Modeled on themes/alpha.css, define both light and dark variants:
@layer theme {
:root {
@variant ocean-light {
--radius: 0.5rem;
--background: oklch(0.97 0.01 220);
--foreground: oklch(0.15 0.03 220);
--surface: oklch(0.97 0.01 220);
--surface-foreground: var(--foreground);
--surface-secondary: oklch(0.93 0.02 220);
--surface-secondary-foreground: var(--foreground);
--surface-tertiary: oklch(0.90 0.02 220);
--surface-tertiary-foreground: var(--foreground);
--overlay: oklch(0.97 0.01 220);
--overlay-foreground: var(--foreground);
--muted: var(--color-neutral-500);
--default: oklch(0.92 0.02 220);
--default-foreground: oklch(0.15 0.03 220);
--accent: oklch(0.45 0.15 220);
--accent-foreground: var(--snow);
--field-background: var(--default);
--field-foreground: var(--foreground);
--field-placeholder: var(--muted);
--field-border: transparent;
--success: oklch(0.55 0.12 154);
--success-foreground: var(--snow);
--warning: oklch(0.72 0.15 65);
--warning-foreground: var(--eclipse);
--danger: oklch(0.63 0.19 29);
--danger-foreground: var(--snow);
--segment: oklch(0.97 0.01 220);
--segment-foreground: var(--eclipse);
--border: oklch(0.85 0.03 220);
--separator: oklch(0.75 0.03 220);
--focus: var(--accent);
--link: var(--foreground);
--surface-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
0 0 1px 0 rgba(0, 0, 0, 0.06);
--overlay-shadow:
0 2px 8px 0 rgba(0, 0, 0, 0.02), 0 14px 28px 0 rgba(0, 0, 0, 0.03);
--field-shadow:
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
0 0 1px 0 rgba(0, 0, 0, 0.06);
}
@variant ocean-dark {
--radius: 0.5rem;
--background: oklch(0.12 0.03 220);
--foreground: oklch(0.92 0.02 220);
--surface: oklch(0.17 0.03 220);
--surface-foreground: var(--foreground);
--surface-secondary: oklch(0.22 0.03 220);
--surface-secondary-foreground: var(--foreground);
--surface-tertiary: oklch(0.25 0.03 220);
--surface-tertiary-foreground: var(--foreground);
--overlay: oklch(0.20 0.03 220);
--overlay-foreground: var(--foreground);
--muted: var(--color-neutral-400);
--default: oklch(0.20 0.03 220);
--default-foreground: var(--snow);
--accent: oklch(0.65 0.15 220);
--accent-foreground: var(--eclipse);
--field-background: var(--default);
--field-foreground: var(--foreground);
--field-placeholder: var(--muted);
--field-border: transparent;
--success: oklch(0.55 0.12 154);
--success-foreground: var(--snow);
--warning: oklch(0.85 0.14 78);
--warning-foreground: var(--eclipse);
--danger: oklch(0.58 0.16 31);
--danger-foreground: var(--snow);
--segment: oklch(0.20 0.03 220);
--segment-foreground: var(--foreground);
--border: oklch(0.25 0.03 220);
--separator: oklch(0.35 0.03 220);
--focus: var(--accent);
--link: var(--foreground);
--surface-shadow: 0 0 0 0 transparent inset;
--overlay-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.2) inset;
--field-shadow: 0 0 0 0 transparent inset;
}
}
}Every theme must declare exactly the same variable names. Cross-check against
alpha.cssfor the full list.
2. Import in global.css
@import "./themes/alpha.css";
@import "./themes/lavander.css";
@import "./themes/mint.css";
@import "./themes/sky.css";
@import "./themes/ocean.css"; /* add this */3. Register in metro.config.js
module.exports = withUniwindConfig(config, {
cssEntryFile: "./global.css",
dtsFile: "./uniwind-types.d.ts",
extraThemes: [
"alpha-light", "alpha-dark",
"lavender-light", "lavender-dark",
"mint-light", "mint-dark",
"sky-light", "sky-dark",
"ocean-light", "ocean-dark", // add these
],
});After editing metro.config.js, restart Metro. If you see stale styles, run npx expo start --clear.
Reference: Uniwind custom themes docs
Add i18n translations (optional)
Add display names and descriptions for the new family in the locale message files:
{
"settings": {
"themeFamilyOptions": {
"ocean": "Ocean"
},
"themeFamilyDescriptions": {
"ocean": "Deep ocean blues"
}
}
}