EasyStarter logoEasyStarter

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:

ValueDescription
systemFollow the device system setting (default)
lightForce light mode
darkForce dark mode

Theme Family (ThemeFamily)

Controls the overall color tone. Four families are built in:

ValueStyle
alphaDefault family
lavenderSoft purple
mintFresh green
skyClear 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

FileDescription
apps/native/providers/theme-provider.tsxTheme state, persistence, and Uniwind sync
apps/native/configs/app-config.tsStorage key names

User Preference Storage

Theme choices are persisted to device storage via AsyncStorage:

Storage key (from app-config.ts)Content
{AppName}_theme_preferenceAppearance mode: system / light / dark
{AppName}_theme_familyTheme 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:

apps/native/providers/theme-provider.tsx
const [themeModePreference, setThemeModePreferenceState] =
  useState<ThemeModePreference>("system");   // default: follow system

const [themeFamily, setThemeFamilyState] = useState<ThemeFamily>("alpha");  // default: alpha

Change 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:

apps/native/providers/theme-provider.tsx
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:

apps/native/themes/ocean.css
@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.css for the full list.

2. Import in global.css

apps/native/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

apps/native/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:

packages/i18n/messages/native/en.json
{
  "settings": {
    "themeFamilyOptions": {
      "ocean": "Ocean"
    },
    "themeFamilyDescriptions": {
      "ocean": "Deep ocean blues"
    }
  }
}