@codehogs/react-native-theme-engine
v1.0.0
Published
Semantic theming, system mode, persistence, and React Navigation integration for React Native.
Maintainers
Readme
@codehogs/react-native-theme-engine
Theme is product state, not just colors. This library gives you:
light|dark|systemwith first-class OS following via React Native’suseColorScheme- Semantic design tokens (background, surfaces, text roles, borders, states)
- One derived navigation theme aligned with app tokens (React Navigation
NavigationContainer) - Optional persistence through a small storage adapter (no hard-coded AsyncStorage in core)
- Minimal rerenders: one context value;
useThemedStylesmemoizes on the resolvedthemeobject
Peers: react, react-native (≥0.72). @react-navigation/native is optional at install time but recommended when you use navigators.
Install
npm install @codehogs/react-native-theme-engine react react-nativeOptional (navigation):
npm install @react-navigation/nativeOptional (persistence example):
npm install @react-native-async-storage/async-storageQuick start
import {
ThemeEngineProvider,
useAppTheme,
defaultLightTokens,
defaultDarkTokens,
} from "@codehogs/react-native-theme-engine";
export default function App() {
return (
<ThemeEngineProvider
light={defaultLightTokens}
dark={defaultDarkTokens}
defaultMode="system"
>
<Root />
</ThemeEngineProvider>
);
}
function Root() {
const { theme, mode, resolvedMode, setMode } = useAppTheme();
return (
/* use theme.tokens.* for styles */
);
}Architecture (layers)
- Raw palettes — Your hex /
PlatformColorchoices live inside token files. - Semantic tokens —
ThemeTokens:colors,spacing,radius,typographywith roles liketextPrimary,surfaceElevated,borderFocus. - Component tokens — Use
mergeTokensor app-specific wrappers to add button/input/modal slices without polluting core. - Runtime engine state —
mode(user preference),resolvedMode(aftersystem),theme(AppThemewithnavigation).
API reference
ThemeEngineProvider
| Prop | Type | Description |
|------|------|-------------|
| light | ThemeTokens | Tokens when resolved mode is light. |
| dark | ThemeTokens | Tokens when resolved mode is dark. |
| defaultMode | "light" \| "dark" \| "system" | Initial preference before storage hydrates. Default "system". |
| themeId | string | Identifier on AppTheme.id. Default "default". |
| storage | { get, set } | Optional persistence (see below). |
Children should read useAppTheme() below this provider.
useAppTheme()
Returns:
| Field | Description |
|-------|-------------|
| theme | AppTheme: id, mode (resolved "light" | "dark"), isDark, tokens, navigation. |
| mode | User preference: "light" | "dark" | "system". |
| resolvedMode | Effective mode after resolving system using the OS scheme. |
| setMode(mode) | Sets preference and persists when storage is configured. |
| toggleMode() | Switches between explicit light and dark (not system). |
| isHydrated | false until async storage.get() finishes (always true if no storage). Use to avoid theme flash on cold start. |
useThemeMode()
Subset: mode, resolvedMode, setMode, toggleMode, isHydrated.
createThemeEngine(config)
Returns an isolated engine with its own React context:
Provider— same props asThemeEngineProviderminus duplicate config (config is closed over).useAppTheme/useThemeMode— scoped to that provider.context— advanced testing / bridging.
Use when you need multiple engines or Storybook isolation.
getNavigationTheme(appTheme)
Returns appTheme.navigation, suitable for <NavigationContainer theme={...} />. Keeps navigation colors in sync with semantic tokens.
Also available: getNavigationThemeFromTokens(resolvedMode, tokens) and buildNavigationTheme(resolvedMode, tokens) for tests or tooling.
useThemedStyles(factory)
import { StyleSheet } from "react-native";
import { useThemedStyles } from "@codehogs/react-native-theme-engine";
const styles = useThemedStyles((theme) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: theme.tokens.colors.background,
},
})
);Recomputes when theme changes (mode switch or new token references). Prefer a stable factory (module-level function or useCallback) if you extract it.
Token helpers
defaultLightTokens/defaultDarkTokens— Opinionated defaults (soft neutrals, not pure #000/#fff backgrounds).mergeTokens(base, patch)— Shallow merge per section (colors,spacing,radius,typography).withPrimaryBrand(tokens, primaryHex, { primaryText? })— Dynamic brand primary with simple contrast default forprimaryText.
Persistence: createThemePreferenceStorage(storage, key?)
Pass anything with getItem / setItem (e.g. AsyncStorage, MMKV):
import AsyncStorage from "@react-native-async-storage/async-storage";
import {
ThemeEngineProvider,
createThemePreferenceStorage,
} from "@codehogs/react-native-theme-engine";
const themeStorage = createThemePreferenceStorage(AsyncStorage, "@myapp/theme-mode");
export default function App() {
return (
<ThemeEngineProvider
light={light}
dark={dark}
defaultMode="system"
storage={themeStorage}
>
{/* ... */}
</ThemeEngineProvider>
);
}React Navigation
import { NavigationContainer } from "@react-navigation/native";
import { useAppTheme, getNavigationTheme } from "@codehogs/react-native-theme-engine";
function AppNavigation() {
const { theme } = useAppTheme();
return (
<NavigationContainer theme={getNavigationTheme(theme)}>
{/* navigators */}
</NavigationContainer>
);
}theme.navigation matches React Navigation’s theme shape (dark, colors.primary, colors.background, colors.card, colors.text, colors.border, colors.notification). Navigator UI and your screens share the same source of truth.
Customizing themes
Define full ThemeTokens for each mode, or extend defaults:
import {
defaultLightTokens,
defaultDarkTokens,
mergeTokens,
} from "@codehogs/react-native-theme-engine";
const light = mergeTokens(defaultLightTokens, {
colors: {
primary: "#7C3AED",
primaryText: "#FFFFFF",
},
});
const dark = mergeTokens(defaultDarkTokens, {
colors: {
primary: "#A78BFA",
primaryText: "#1E1B4B",
},
});For platform-native colors, use PlatformColor inside your token objects where a string is expected (same as any React Native style).
React Native Paper (optional)
getPaperThemeOverrides(appTheme) returns a partial you can merge with Paper’s MD3 theme. Refine fonts and component mappings in your app; Paper is not a required dependency.
Accessibility notes
- Prefer semantic roles (
textMuted,textDisabled,borderFocus) over raw grays so contrast stays intentional. - Do not rely on color alone for errors; pair
dangerwith icons / copy. - Keep spacing and typography in tokens so touch targets stay consistent across themes.
Troubleshooting
- Flash on launch with storage: Wait for
isHydrated === truebefore rendering themed UI, or show a neutral splash. - Too many rerenders: Define
light/darkat module scope or memoize; avoid recreating token objects every render. - System mode:
resolvedModeupdates when the OS appearance changes;modestays"system"until the user picks a fixed mode.
Example
See example/App.example.js for NavigationContainer, AsyncStorage, and useThemedStyles together.
License
MIT.
