react-native-theme-transition
Smooth, animated theme transitions for React Native. Captures a screenshot, overlays it, switches colors underneath, and fades out — all in JS via Reanimated. Expo Go compatible. No native code required.
Installation
Expo (SDK 54+): npx expo install react-native-theme-transition react-native-worklets
Add react-native-worklets/plugin as the last plugin in babel.config.js.
On SDK 55+, do NOT add react-native-reanimated/plugin — babel-preset-expo already includes it.
Key rules
- All themes must share identical token keys — use
Record<keyof typeof light, string>. 'system'is reserved — cannot be a theme key.- Provider wraps everything — content outside won't be in the screenshot.
initialThemeis read once — use a bridge component for external stores.setThemeduring a transition returnsfalse— useisTransitioningto disable buttons.onThemeChangeis the only guaranteed callback —onTransitionEndskips on capture failure.- Don't change styles based on
isTransitioning— the screenshot captures current visuals. - No native
<Switch>— use a custom toggle withuseTheme({})and plain React styles. - No
style={({ pressed }) => (...)}— use staticstyle={{...}}. The screenshot captures the current visual state; if the pressed state at capture time differs from when the overlay fades out, it causes a visible flash. - Selection tracking: any component whose visual state changes on theme switch must use
useTheme({})oruseTheme({ initialSelection }). - Hydration-only bridge when using
select():select()updates the visual selection state, then deferssetTheme()by one frame. A reactive bridge (useEffect → setThemeon every store change) races with this deferred timing and reverts the pill. Use a bridge that syncs once on hydration, then let the picker callselect()+ store setter together inonPress. - Picker highlight from
selected, not the store:selected(fromuseTheme({ initialSelection })) updates synchronously before the screenshot capture. Store state updates asynchronously and would show the wrong highlight.
Which hook should I use?
| Scenario | Call |
|---|---|
| Just need colors | useTheme().colors (or useColors() shorthand) |
| Need theme name + colors + setTheme | useTheme() |
| Component has a visual indicator that changes on selection (toggle thumb, pill, checkmark) | useTheme({}) or useTheme({ initialSelection }) |
| Visual indicator + external persistence (Zustand, Redux, MMKV) | useTheme({ initialSelection: storeValue }) with hydration-only bridge |
How it works (30-second version)
setTheme()is called → touch input blocked immediately (Reanimated shared value)- Library waits 2 frames for React to paint any pending state changes
- Screenshot of current UI is captured and placed as an overlay at full opacity
- Colors are switched underneath (React context update → all children re-render with new colors)
- Overlay fades out over
durationms → user sees smooth crossfade - Overlay removed, touch unblocked,
isTransitioningreturns tofalse
Reference guides
| You need to... | Read | |---|---| | Full API details, all options, callback ordering, exported types | references/api.md | | Set up a new project from scratch (Expo or CLI) | references/new-project.md | | Migrate from Context, Zustand, Redux, etc. | references/existing-project.md | | Recipes: system theme, persistence, haptics, React Navigation, Expo Router, modals, multi-theme, StatusBar, analytics | references/recipes.md | | Debug issues: stuck overlay, flash, type errors, system theme not working | references/troubleshooting.md |
Scan to join WeChat group