Description
On iOS 26, tabBarMinimizeBehavior: 'onScrollDown' never engages when the tab's content is a nested (native stack) navigator and/or the scrolling content is a virtualized list (FlashList, LegendList) or any scroll view behind styled wrapper views — i.e. the common shape of a production app. The tab bar never minimizes, and a bottom accessory never transitions to its inline placement.
#3954 covered a narrow variant of this (scroll view wrapped in a single styled View) and was closed with the collapsable={false} workaround. That workaround can't help here: with a nested stack the first-descendant chain from the TabsScreen runs through the stack host and screen views, and with virtualized lists + headers the UIScrollView is simply not on the first-subview chain at all — there is nothing the app author can collapsable={false} their way out of.
Mechanism
UIKit resolves the scroll view that drives the minimize behavior by asking the selected tab's view controller for its bottom-edge content scroll view (contentScrollView(for:) / automatic detection). UIKit's automatic detection cannot find a UIScrollView nested inside a React Native screen tree, so it tracks nothing and UITabBarController.tabBarMinimizeBehavior is inert.
Reproduction shape
NativeTabs (tabBarMinimizeBehavior: onScrollDown)
└─ Tab
└─ native stack navigator
└─ screen
└─ styled wrappers / headers
└─ FlashList / LegendList / ScrollView
RNS 4.25.2, RN 0.85, iOS 26, new arch. (Same report exists for react-native-bottom-tabs: callstack/react-native-bottom-tabs#496, where swizzling contentScrollView(for:) is the known fix.)
Proposed fix (running in production via a patch)
Implement contentScrollViewForEdge: — UIKit's documented override point for exactly this — on RNSTabsScreenViewController and RNSScreen, falling back to a breadth-first search when UIKit's own resolution returns nil. Restricted to the bottom edge so top-edge/navigation-bar behaviors are untouched:
- (UIScrollView *)contentScrollViewForEdge:(NSDirectionalRectEdge)edge
{
UIScrollView *scrollView = [super contentScrollViewForEdge:edge];
if (scrollView != nil) {
return scrollView;
}
if (edge == NSDirectionalRectEdgeBottom) {
return [RNSScrollViewFinder findScrollViewBreadthFirstFrom:self.view];
}
return nil;
}
plus a new finder on RNSScrollViewFinder (the existing first-descendant-chain walk is what fails on real screens):
// Bounded BFS; skips horizontal-only scrollers (carousels) so the tab bar
// tracks vertical scrolling. ~2000-view visit cap as a safety bound.
+ (nullable UIScrollView *)findScrollViewBreadthFirstFrom:(nullable UIView *)view;
With both overrides in place (covering whichever VC UIKit queries — tab root or the visible nested-stack screen), minimize + inline accessory work with nested stacks and LegendList/FlashList content, verified on a physical iOS 26 device.
Notes / open questions for maintainers:
- The
RNSScreen override applies to all stack screens, not only those inside tabs. We saw no regressions (bottom-edge only), but you may prefer gating it to tab-hosted screens or behind a prop.
- An alternative shape would be extending the
RNSContentScrollViewProviding delegation so the BFS lives behind the existing provider protocol.
Happy to open a PR with this.
Description
On iOS 26,
tabBarMinimizeBehavior: 'onScrollDown'never engages when the tab's content is a nested (native stack) navigator and/or the scrolling content is a virtualized list (FlashList, LegendList) or any scroll view behind styled wrapper views — i.e. the common shape of a production app. The tab bar never minimizes, and a bottom accessory never transitions to its inline placement.#3954 covered a narrow variant of this (scroll view wrapped in a single styled
View) and was closed with thecollapsable={false}workaround. That workaround can't help here: with a nested stack the first-descendant chain from theTabsScreenruns through the stack host and screen views, and with virtualized lists + headers theUIScrollViewis simply not on the first-subview chain at all — there is nothing the app author cancollapsable={false}their way out of.Mechanism
UIKit resolves the scroll view that drives the minimize behavior by asking the selected tab's view controller for its bottom-edge content scroll view (
contentScrollView(for:)/ automatic detection). UIKit's automatic detection cannot find aUIScrollViewnested inside a React Native screen tree, so it tracks nothing andUITabBarController.tabBarMinimizeBehavioris inert.Reproduction shape
RNS 4.25.2, RN 0.85, iOS 26, new arch. (Same report exists for react-native-bottom-tabs: callstack/react-native-bottom-tabs#496, where swizzling
contentScrollView(for:)is the known fix.)Proposed fix (running in production via a patch)
Implement
contentScrollViewForEdge:— UIKit's documented override point for exactly this — onRNSTabsScreenViewControllerandRNSScreen, falling back to a breadth-first search when UIKit's own resolution returns nil. Restricted to the bottom edge so top-edge/navigation-bar behaviors are untouched:plus a new finder on
RNSScrollViewFinder(the existing first-descendant-chain walk is what fails on real screens):With both overrides in place (covering whichever VC UIKit queries — tab root or the visible nested-stack screen), minimize + inline accessory work with nested stacks and LegendList/FlashList content, verified on a physical iOS 26 device.
Notes / open questions for maintainers:
RNSScreenoverride applies to all stack screens, not only those inside tabs. We saw no regressions (bottom-edge only), but you may prefer gating it to tab-hosted screens or behind a prop.RNSContentScrollViewProvidingdelegation so the BFS lives behind the existing provider protocol.Happy to open a PR with this.