Repository navigation
Description
Version
v5
Reanimated Version
v3
Gesture Handler Version
v2
Platforms
iOS
What happened?
The template's dropdowns don't list Reanimated 4 or Gesture Handler 3 yet. Actual versions: @gorhom/bottom-sheet 5.2.14, Reanimated 4.7.1, react-native-worklets 0.13.0, Gesture Handler 3.3.0, Expo SDK 58, React Native 0.88.1 (New Architecture).
When the device rotates while a BottomSheetModal with topInset is open, animatedLayoutState.containerHeight keeps the inset from the previous orientation:
Rotation (iPhone 17 Pro, topInset={insets.top}) |
Expected containerHeight |
Actual | Effect |
|---|---|---|---|
| landscape → portrait (inset 0 → 62) | 812 (874 − 62) | 874 | footer pushed 62pt below the screen edge |
| portrait → landscape (inset 62 → 0) | 402 | 340 | footer floats 62pt above the bottom |
Closing and reopening the sheet in the new orientation always gives the right height. Any modal sheet whose topInset comes from the safe area is affected.
Video (iPhone 17 Pro simulator, stock 5.2.14, both directions): https://github.com/abisalde/gorhom-sheet-rotation-repro/blob/main/media/rotation-bug.mp4
Cause. useAnimatedLayout subtracts topInset + bottomInset in the react callback of its two useAnimatedReactions, but their prepare callbacks return only the raw container height. On rotation the window height and the safe-area inset arrive in separate updates. When the inset changes, the reaction restarts with the new closure. The height it returns is equal to previous, because Reanimated keeps previous across restarts, so it returns early and the new inset is never applied. The [state, verticalInset, modal] dependency array doesn't help, because Reanimated ignores it on native (see #2785).
Suggested fix. Return the inset-adjusted height from prepare, so an inset change on its own produces a new value. Applied to both reactions:
useAnimatedReaction(
- () => containerLayoutState?.get().height,
- (result, previous) => {
- if (!result || result === previous) {
+ () => {
+ const height = containerLayoutState?.get().height;
+ if (!height || height === INITIAL_LAYOUT_VALUE) {
+ return INITIAL_LAYOUT_VALUE;
+ }
+ return modal ? height - verticalInset : height;
+ },
+ (result, previous) => {
+ if (result === previous) {
return;
}
if (result === INITIAL_LAYOUT_VALUE) {
return;
}
state.modify(_state => {
'worklet';
- _state.containerHeight = modal ? result - verticalInset : result;
+ _state.containerHeight = result;
return _state;
});
},
[state, verticalInset, modal]
);The rawContainerHeight reaction above it gets the same change. With this patch, both rotations in the repro give the expected heights. We run it as a pnpm patch in our app and checked it on iOS and Android, including foldables. It doesn't overlap with #2786, which only changes the initial state. I'm happy to open a PR.
Reproduction steps
- Run the repro on an iPhone with a notch or Dynamic Island, with rotation lock off. The sheet prints
useBottomSheetInternal()values. - Rotate to landscape.
- Tap Open sheet.
- Rotate to portrait.
- Expected:
containerHeight 812, red footer fully visible. Actual:containerHeight 874, footer cut off at the bottom edge.
Reproduction sample
https://github.com/abisalde/gorhom-sheet-rotation-repro
Snack's newest runtime is SDK 56, two versions behind the Expo SDK 58 / Reanimated 4.7 stack this happens on, so the reproduction is a minimal Expo Go app instead: npm install && npx expo start. The whole repro is App.tsx (~150 lines), so it can be pasted into a Snack if you prefer.
Relevant log output
N/A: no errors; the wrong value is visible in the on-screen readout.
Activity
github-actions commentedon Nov 8, 2026
Hello @abisalde 👋, this issue is being automatically closed and locked because it does not follow the issue template.