How It Works
The screenshot-overlay technique behind smooth theme transitions.
The library captures a full-screen screenshot of the current UI, displays it as an opaque overlay, switches all color tokens underneath, then fades the overlay out on the native UI thread via Reanimated. The screenshot is taken before the color switch, so the transition is seamless.
Sequence diagram
Step by step
setTheme('dark')is called- Touches blocked instantly via a Reanimated shared value (no React re-render needed)
- Two frames wait for the JS → Shadow Tree → Native UI pipeline to fully paint pending state changes
- Full-screen screenshot captured via
react-native-view-shot - Screenshot displayed as an opaque overlay
Image.onLoadconfirms the bitmap is decoded (event-based, not frame-guessing)- One frame for the compositor to paint the overlay on screen
- Color tokens switched underneath
- One frame for React to commit the new theme under the still-opaque overlay
- Overlay fades out on the UI thread via
react-native-reanimated— the RN repaint pipeline completes during the first frames of the fade, when the overlay is still near-opaque - Touches unblocked and overlay removed once the fade completes via a worklet callback (
react-native-worklets)
The frame pipeline
React Native renders in a pipeline:
JS Thread → Shadow Tree → Native UI ThreadEach stage takes ~1 frame (~16ms at 60Hz, ~8ms at 120Hz). The library uses waitFrames
to ensure each stage completes before proceeding:
- 2 frames before capture: ensures pending state changes are fully painted
- 1 frame after overlay mount: ensures the compositor has painted the overlay
- 1 frame after color switch: ensures React has committed the new theme
Why screenshots?
Other approaches to theme transitions require native modules (custom iOS/Android code) to manipulate the view hierarchy directly. The screenshot approach works entirely in JS
- Reanimated, making it compatible with Expo Go and any React Native setup.
The trade-off is that the overlay is static — dynamic content (videos, animations) will appear frozen during the ~350ms fade.