Repository navigation
[iOS] pager content getting cropped on version 8.0.4 #1099
Description
Activity
Same root cause, but we hit a second failure mode that I think makes the design flaw clearer: the injected inset isn't just transient — when the ancestor that
nearestNonZeroSafeAreaInsets()lands on is a scroll container, the value is wrong by construction and tracks the scroll offset.Setup
PagerView (tabs) └─ vertical ScrollView └─ PagerView (image pager, ~500pt tall card mid-screen) └─ page: <Image />react-native-pager-view8.0.4 ·react-native0.84.1 · Fabric · iOS 26.1Symptom
Scrolling the outer vertical ScrollView, the image inside the pager stops moving and appears pinned to the screen while everything around it (card border, overlays, page indicator — all siblings outside
<PagerView>) scrolls normally. Once the value saturates or resolves to a different ancestor, the image snaps back and starts scrolling again.Why
Walking up from
PageChildViewController.view, every nearby ancestor sits inside the safe area, so the loop keeps going until it reaches the outer ScrollView's content view. That view's top edge is above the safe-area boundary by exactly the scroll offset, so itssafeAreaInsets.topgrows 1:1 as you scroll.Because
safeAreaInsetsis derived from on-screen geometry,viewSafeAreaInsetsDidChangefires continuously while scrolling, soadditionalSafeAreaInsets.topis re-applied every pass:container scrolls up by N → page content pushed down by N → net zero → content looks frozenNo layout-timing flicker involved here — the value is stable, it's just not the page's inset. The 0.5pt guard is irrelevant for the same reason you noted.
Verified
Three independent changes each remove the symptom:
- Making
propagateSafeArea()a no-op (patched locally) - Replacing
<PagerView>with a pagingFlatList(never enters this code path) - Downgrading to 8.0.3, which uses
UIHostingController(rootView:, ignoreSafeArea: true)instead
Affected versions
ios/Extensions.swiftis byte-identical in 8.0.4, 8.0.5 and 9.0.0 — so 9.0.0 does not fix this.Suggestion
An ancestor's
safeAreaInsetscan't stand in for the page's own: it's computed against that view's bounds and position, so it means something different for every ancestor, and it changes as the ancestor moves. Two directions that avoid both failure modes:- compute the page view's own overlap with the window's unsafe regions (
view.convert(...)againstwindow.safeAreaLayoutGuide) instead of copying an ancestor's value, or - restore the 8.0.3
ignoreSafeArea: truepath —disableSafeArea()is still defined inExtensions.swiftbut no longer called — and address [iOS] New Arch: safe area insets are stripped from PagerView children #1090 without re-injection.
Happy to test a patch against the nested-pager-in-ScrollView case if that helps.
- Making
Hello @kyunss 👋
Thank you for that analysis. Could you provide a PR for that with the example inside this repository?- addedResolution: Needs ReproThis issue could be improved with a demo to reproduce the issue.This issue could be improved with a demo to reproduce the issue.
on Aug 14, 2026 I am not able to reproduce that using the below example
import React from 'react'; import { Image, ScrollView, StyleSheet, Text, View } from 'react-native'; import PagerView from 'react-native-pager-view'; import { IMAGE_URIS } from './utils'; const CARD_HEIGHT = 500; /** * Reproduces https://github.com/callstack/react-native-pager-view/issues/1099 * * On iOS with Fabric and pager-view >= 8.0.4, scroll the first tab vertically. * The image in the nested pager can appear pinned while the card border and page * indicator continue moving. This happens because the page controller copies the * outer ScrollView content view's changing safe-area inset. */ export function Issue1099SafeAreaRepro() { return ( <PagerView style={styles.tabs} initialPage={0} testID="issue-1099-tabs"> <ScrollView key="scrollable-tab" style={styles.tab} contentContainerStyle={styles.scrollContent} testID="issue-1099-outer-scroll-view" > <Text style={styles.heading}>Issue #1099: nested pager safe area</Text> <Text style={styles.description}> Scroll this tab. On affected iOS Fabric builds, the image may stop moving with its card temporarily while the card border and indicator continue scrolling. </Text> <View style={styles.topSpacer} /> <View style={styles.card} testID="issue-1099-card"> <PagerView style={styles.imagePager} initialPage={0} testID="issue-1099-image-pager" > {IMAGE_URIS.slice(0, 3).map((uri, index) => ( <View key={uri} collapsable={false} style={styles.imagePage} testID={`issue-1099-image-page-${index}`} > <View collapsable={false} style={{ width: '100%', height: '100%', backgroundColor: 'red', justifyContent: 'space-between', }} > <Text>TOP</Text> <Text>BOTTOM</Text> </View> </View> ))} </PagerView> <View style={styles.indicator} testID="issue-1099-page-indicator"> <Text style={styles.indicatorText}>Swipe images horizontally</Text> </View> </View> {Array.from({ length: 8 }, (_, index) => ( <View key={index} style={styles.row}> <Text>Scrollable content {index + 1}</Text> </View> ))} </ScrollView> <View key="second-tab" style={styles.secondTab} collapsable={false}> <Text>Second tab</Text> </View> </PagerView> ); } const styles = StyleSheet.create({ tabs: { flex: 1, }, tab: { flex: 1, backgroundColor: '#f5f5f5', }, scrollContent: { padding: 16, paddingBottom: 48, }, heading: { fontSize: 20, fontWeight: '600', }, description: { marginTop: 8, color: '#444', lineHeight: 20, }, topSpacer: { height: 180, }, card: { height: CARD_HEIGHT, borderWidth: 3, borderColor: '#5b5bd6', backgroundColor: 'white', }, imagePager: { height: CARD_HEIGHT - 48, }, imagePage: { flex: 1, }, image: { width: '100%', height: '100%', }, indicator: { height: 42, alignItems: 'center', justifyContent: 'center', backgroundColor: '#e5e5ff', }, indicatorText: { fontWeight: '600', }, row: { height: 120, marginTop: 16, alignItems: 'center', justifyContent: 'center', borderRadius: 12, backgroundColor: 'white', }, secondTab: { flex: 1, alignItems: 'center', justifyContent: 'center', }, });Another failure mode of the same
PageChildViewController.propagateSafeArea()change, with a drop-in example forexample/src/.I think this one isolates the design problem: the injected inset does not merely carry a wrong value, it moves the page content. Since
RepresentableViewbecame aUIViewControllerRepresentable, settingadditionalSafeAreaInsetson that controller makes SwiftUI lay the representable out inside the safe area — the hosted RN view is pushed down until its top sits on the safe-area boundary, while anything that is a sibling of<PagerView>stays where it is.Environment
- react-native-pager-view: 8.0.5 — correct on 8.0.3, broken from 8.0.4 (9.0.0–9.0.2 ship the same
Extensions.swift) - react-native: 0.86.3 (Expo SDK 57)
- Architecture: Fabric / New Architecture
- iOS 26.3, iPhone 17 Pro simulator (any device with a non-zero top inset)
Steps to reproduce
- Save the file below as
example/src/Issue1099PageOffsetRepro.tsx. - In
example/src/App.tsx, import it and add one entry toghIssues:import { Issue1099PageOffsetRepro } from './Issue1099PageOffsetRepro'; const ghIssues: Example[] = [ // … { component: Issue1099PageOffsetRepro, name: 'Issue #1099 Page Offset Repro', }, ];
cd example/ios && pod install, then build and run on iOS with the New Architecture.- Open Issue [iOS] pager content getting cropped on version 8.0.4 #1099 Page Offset Repro from the list.
The screen draws two 44pt bars, both at
top: 0of the same container. The magenta one is a sibling of<PagerView>; the lime one is the first child of page 0. They must form a single continuous stripe.Expected (8.0.3): the two halves meet, and
[magenta|outside][lime|inside]reads as one 44pt stripe across the top of the screen.Actual (8.0.5): the magenta half stays at
y = 0while the lime half sits aty = 62, exactly the status-bar inset.react-native-pager-view outside bar inside bar offset 8.0.3 y = 0 y = 0 0 ✅ 8.0.5 y = 0 y = 62 62 ❌ If it does not reproduce, check these
These are the things that silently mask it — I hit most of them while narrowing it down:
- The screen must have no header. This is the big one. The example app's screens are pushed with a header, and under an opaque header the pager already starts below the safe-area boundary, so there is nothing left to inset and the bars line up even on 8.0.5. The repro file hides the header itself via
navigation.setOptions({ headerShown: false })— please keep thatuseLayoutEffect. (AheaderTransparent: truescreen reproduces it too, and that is how we originally hit it.) - Rebuild natively when switching versions.
yarn add react-native-pager-view@8.0.3+ a JS reload proves nothing; it needspod installand a fresh native build. - iOS + Fabric. The example app can run either stack; the JS/native stack toggle in the header does not matter, but the New Architecture does.
- A device with a non-zero top safe-area inset. The offset is that inset, so on a device reporting 0 there is nothing to see.
- Don't try to measure it from JS.
onLayoutandmeasureInWindowboth reporty: 0for the page root — the shift only exists in UIKit. Compare the two bars visually or diff screenshots.
The page lands on the safe-area boundary, wherever the pager starts
Same app, same 8.0.5 build, moving only the pager container's top edge. Measured on a screen with a transparent native header, where the enclosing
react-native-screensscreen reports a 100.33pt top inset:pager frame top lime bar lands at offset 0 100.33 100.33 40 100.33 60.33 60 100.33 40.33 120 120 0 So the trigger is precisely the PagerView's frame overlapping the inset region — which a full-screen pager always does, and which is also why the last row (pager pushed below the boundary) looks fine.
Why it is easy to miss
In the most common layout — pager fills the screen, all chrome inside it — a uniform inset just looks like correct safe-area handling. It only becomes visible when something outside the pager has to line up with something inside it.
That is exactly how the "collapsible header + sticky tab bar + one scroll view per page" pattern works (bluesky's
PagerWithHeaderand everything derived from it): the header and the sticky tab bar are absolutely positioned siblings of<PagerView>attop: 0, and each page's scroll view offsets its own content withcontentContainerStyle={{ paddingTop: headerHeight }}. Both sides are expressed in the pager container's coordinate space, so insetting only the pages leaves a permanent gap between the sticky tab bar and the page content — 100pt on our screens, with no error and nothing else on the screen looking wrong.Suggestion
The goal of #1085 / #1086 — scroll views inside pages seeing real
safeAreaInsets— does not require the page host to participate in layout. Some directions (I have only verified the diagnosis, not a fix):- keep
RepresentableViewas aUIViewRepresentableand surface the insets without a view controller, so nothing gets re-laid-out; - or keep the controller but stop SwiftUI from insetting the representable itself;
- or add an opt-out prop — a pager whose page offsets are computed in JS never wants the inset.
Happy to turn the file below into a PR if that helps.
// example/src/Issue1099PageOffsetRepro.tsx import * as React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import PagerView from 'react-native-pager-view'; /** * Repro for the page-offset failure mode of * https://github.com/callstack/react-native-pager-view/issues/1099 * * The magenta bar and the PagerView are SIBLINGS in the same container, and * both bars are placed at `top: 0` of that container, so they must line up * into a single continuous stripe: [magenta|outside][lime|inside] * * <= 8.0.3 one continuous stripe * >= 8.0.4 (iOS) the lime half is pushed down onto the safe-area boundary * * The header is hidden ON PURPOSE, and it is load-bearing for the repro: the * bug only appears when the PagerView's own frame overlaps the safe-area inset * region. Under a normal (opaque) navigation header the pager already starts * below the boundary, there is nothing left to inset, and the bars line up even * on a broken version. Do not drop the `useLayoutEffect` below. * * Also note: `onLayout` and `measureInWindow` both keep reporting `y: 0` for * the page root. The offset is applied by SwiftUI while laying out the * `UIViewControllerRepresentable`, so it does not exist as far as JS is * concerned — which is why this repro is visual rather than numeric. */ export function Issue1099PageOffsetRepro() { const navigation = useNavigation(); React.useLayoutEffect(() => { navigation.setOptions({ headerShown: false }); }, [navigation]); return ( <View style={styles.container}> {/* OUTSIDE the pager — absolute, top: 0 of the shared container */} <View style={[styles.bar, styles.outside]}> <Text style={styles.label}>outside</Text> </View> <PagerView style={styles.pager} initialPage={0}> <View key="0" style={styles.page}> {/* INSIDE the pager — first child, top of page 0 */} <View style={[styles.bar, styles.inside]}> <Text style={styles.label}>inside</Text> </View> </View> <View key="1" style={styles.page} /> </PagerView> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#ddd' }, pager: { flex: 1 }, page: { flex: 1 }, bar: { position: 'absolute', top: 0, width: '50%', height: 44, alignItems: 'center', justifyContent: 'center', }, outside: { left: 0, backgroundColor: 'magenta', zIndex: 10 }, inside: { left: '50%', backgroundColor: 'lime' }, label: { fontSize: 13, fontWeight: '600', color: '#000' }, });
- react-native-pager-view: 8.0.5 — correct on 8.0.3, broken from 8.0.4 (9.0.0–9.0.2 ship the same
@curry-888
Thank you for the explanation. Please go ahead and provide a PR for that- removedResolution: Needs ReproThis issue could be improved with a demo to reproduce the issue.This issue could be improved with a demo to reproduce the issue.
on Aug 31, 2026 Hi @troZee , another failure mode with the same symptom (page cropped at the bottom,
onLayoutstill full height), but a different cause frompropagateSafeArea(): SwiftUI keyboard avoidance on theGeometryReaderroot. Still present on 9.0.5, so #1140 does not cover it.What happens
With the keyboard open, every page is shrunk by the keyboard overlap. The lower part of the page is clipped and stops receiving touches. It shows when the pager sits in a container that follows the keyboard with a transform (e.g. a chat composer in
KeyboardStickyView); a full-screen pager hides it, because the cropped area is under the keyboard.Before (9.0.5) After (patched) pager-before-fix.mp4
pager-after-fix.mp4
Why
- This is the [iOS] v8 (SwiftUI TabView): pages shrink when the keyboard appears — real devices only #1096 symptom again. fix(ios): prevent SwiftUI keyboard avoidance from shrinking pages #1097 (
.ignoresSafeArea(.keyboard)) was closed in favour of fix(ios): propagate safe area insets to child UIKit views inside PagerView #1085, which has no keyboard handling. - 8.0.4 was fine because
.ignoresSafeArea()sat on theTabView, which was the root ofPagerView.body, so it covered the keyboard region too. - chore: improve ios and maestro tests #1101 (9.0.2) wrapped the
TabViewin aGeometryReaderand frames every page toproxy.size. TheGeometryReaderis now the root and still honours the keyboard region, soproxy.size.heightdrops when the keyboard opens. - fix(ios): keep pages out of the safe area again #1140 restores
disableSafeArea(), which zeroessafeAreaInsetsonly. Keyboard avoidance is a separate channel.
Fix
} } + .ignoresSafeArea() .onAppear {Verified with a local patch on 9.0.5 (iOS simulator, New Architecture, via react-native-tab-view 4.3.1): pages keep React Native's height with the keyboard open. The inner modifier on the TabView stays, so the vertical layout is unchanged.
Happy to open a PR with an example under example/src/gh-issues/ and a Maestro flow under .maestro/issues/.
- This is the [iOS] v8 (SwiftUI TabView): pages shrink when the keyboard appears — real devices only #1096 symptom again. fix(ios): prevent SwiftUI keyboard avoidance from shrinking pages #1097 (
@seantanys
Thank you for the explanation. Please provide a PR and we can start a discussion there@seantanys Thank you for the explanation. Please provide a PR and we can start a discussion there
Not a problem, I have created a PR #1150 , cheers!
Environment
react-native-pager-view: 8.0.4 (regression from 8.0.2)
react-native: 0.81.1
Architecture: Fabric (New Architecture)
Platform: iOS
Device: iPhone 16 Pro, portrait (reproduces on any device with a home-indicator safe area)
Tab bar: custom React Native bottom tab (not the system UITabBar)
Description
Regression in 8.0.4, introduced by #1085.
After upgrading 8.0.2 → 8.0.4, page content is cropped at the bottom on first render and only corrects after the first scroll.
Root cause (confirmed with native logging inside PageChildViewController):
propagateSafeArea() walks up to the nearest ancestor with a non-zero safe area and copies its safeAreaInsets into additionalSafeAreaInsets. The picked source is consistently the same view (_UIHostingView<_ViewList_View>), but its safeAreaInsets.bottom is not stable during layout — it flickers 49 → 24.33 while the hosting view's height settles through intermediate passes (730 → 681 → 656.66).
propagateSafeArea() samples this value mid-relayout and pins the transient 24.33, cropping the content. The 0.5pt guard doesn't help, since 49 → 24.33 is a large delta (the guard only prevents infinite layout loops, not incorrect values). A manual scroll forces a clean relayout back to 49, which is why the crop disappears after scrolling.
Trigger: the initial layout pass on first mount, and again whenever the page is relaid out. The inset is read while the hosting view's layout is still settling, so an intermediate value gets applied.
Expected: content uses the correct, stable bottom inset (49) on first render.
Actual: content is cropped using a transient inset (24.33) until the first scroll.
Reproducible Demo
Fabric app with a PagerView whose page contains a scrollable list.
The page is rendered inside a view whose height changes during initial layout (below a collapsing header or header height changes during layout).
On first load, the bottom of the content is cut off. Scroll once and it corrects.