Recipes
Expo Router
Full Expo Router setup with typed navigation colors and StatusBar sync.
Root layout
import { ThemeTransitionProvider, useTheme } from '@/lib/theme';
import { ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
function InnerLayout() {
const { colors, name } = useTheme();
return (
<ThemeProvider value={{
dark: name === 'dark',
colors: {
primary: colors.primary,
background: colors.background,
card: colors.card,
text: colors.text,
border: colors.border,
notification: colors.primary,
},
}}>
<StatusBar style={name === 'dark' ? 'light' : 'dark'} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
</Stack>
</ThemeProvider>
);
}
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeTransitionProvider initialTheme="system">
<InnerLayout />
</ThemeTransitionProvider>
</GestureHandlerRootView>
);
}This example uses name === 'dark' for simplicity. If your app has
custom dark-ish themes (e.g. 'ocean', 'midnight'), define a set
and check membership: const DARK_THEMES = new Set(['dark', 'ocean']); dark: DARK_THEMES.has(name).
Key points
ThemeTransitionProviderinsideGestureHandlerRootViewbut wrapping all navigationInnerLayoutmust be inside the provider (it usesuseTheme)initialTheme="system"for OS-following by defaultThemeProviderfrom@react-navigation/nativefeeds typed colors to navigation headers, tab bars, etc.
Bottom sheets and modals
Bottom sheets must be inside the provider:
<ThemeTransitionProvider initialTheme="system">
<BottomSheetModalProvider>
<App />
</BottomSheetModalProvider>
</ThemeTransitionProvider>React Native's built-in Modal renders in a separate native window — it won't
be captured in the screenshot. Theme changes while a modal is open will show
the transition on the background only.