Skip to main content

Dark Mode

The library supports automatic dark mode with system preference detection.

Theme Modes​

ModeBehavior
'system'Follows device settings (default)
'light'Always light mode
'dark'Always dark mode

Changing Theme Mode​

import { useTheme } from '@multinaire/expo-ui';

function ThemeToggle() {
const { themeMode, changeThemeMode } = useTheme();

return (
<View>
<Text>Current: {themeMode}</Text>

<Button title="Light" onPress={() => changeThemeMode('light')} />
<Button title="Dark" onPress={() => changeThemeMode('dark')} />
<Button title="System" onPress={() => changeThemeMode('system')} />
</View>
);
}

Getting Current Theme​

Use useThemeMode() to get the resolved theme (accounts for system preference):

import { useThemeMode } from '@multinaire/expo-ui';

function MyComponent() {
const resolvedTheme = useThemeMode();
// Returns 'light' or 'dark' (never 'system')

return (
<Icon
icon={resolvedTheme === 'dark' ? 'Sun' : 'Moon'}
/>
);
}

Theme Picker Component​

import {
ListPicker,
useTheme,
} from '@multinaire/expo-ui';

const themeOptions = [
{ text: 'System', value: 'system' },
{ text: 'Light', value: 'light' },
{ text: 'Dark', value: 'dark' },
];

function ThemePicker() {
const { themeMode, changeThemeMode } = useTheme();

return (
<ListPicker
selectedItem={themeMode}
items={themeOptions}
onChange={changeThemeMode}
/>
);
}

Persistence​

Theme preference is automatically saved to AsyncStorage. When the app restarts, the user's preference is restored.

Status Bar​

The status bar color automatically updates based on the theme. The library uses expo-system-ui to set the background color.

Browser Chrome (Web)​

On web, MultinaireUI also keeps the browser's own surfaces on the app's resolved mode — no setup required:

  • <meta name="theme-color">, which Safari uses to tint its top bar and bottom toolbar.
  • color-scheme and the html/body background, which paint the over-scroll (rubber-band) areas above and below the document, and tell the browser which scheme to render its own controls and scrollbars in.

This matters once a user picks an explicit 'light' or 'dark' mode: without it the browser keeps following the OS preference, so a light bar and footer end up wrapped around a dark app (and vice versa).

The update runs in a layout effect, so it lands before the first paint — nothing flashes.

Covering the first paint​

The app's own theme is only known after React mounts. To keep the pre-hydration frame from painting the browser's default white, express the theme statically in app/+html.tsx, keyed on prefers-color-scheme:

import { ScrollViewStyleReset } from 'expo-router/html';
import type { PropsWithChildren } from 'react';

import theme from '@/src/assets/theme.json';

export default function Root({ children }: PropsWithChildren) {
const { light, dark } = theme.colors;

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"
/>

<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content={light.background}
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content={dark.background}
/>

<ScrollViewStyleReset />

<style
dangerouslySetInnerHTML={{
__html: `
:root { color-scheme: light; background-color: ${theme.colors.light.background}; }
body { background-color: ${theme.colors.light.background}; }
@media (prefers-color-scheme: dark) {
:root { color-scheme: dark; background-color: ${theme.colors.dark.background}; }
body { background-color: ${theme.colors.dark.background}; }
}`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}

The Expo template ships this file already.

Best Practices​

  1. Design for both modes - Test your UI in both light and dark
  2. Use semantic colors - onBackground instead of hardcoded black/white
  3. Avoid pure black - Use #121212 or similar for better readability
  4. Check contrast ratios - Ensure text is readable in both modes
  5. Test images - Some images may need different versions for each mode