Skip to content

fix(ios): default contentInsetAdjustmentBehavior to scrollableAxes on iOS 26+#57300

Open
IsaacIsrael wants to merge 3 commits into
react:mainfrom
IsaacIsrael:fix/ios26-scroll-view-content-inset-liquid-glass
Open

fix(ios): default contentInsetAdjustmentBehavior to scrollableAxes on iOS 26+#57300
IsaacIsrael wants to merge 3 commits into
react:mainfrom
IsaacIsrael:fix/ios26-scroll-view-content-inset-liquid-glass

Conversation

@IsaacIsrael

@IsaacIsrael IsaacIsrael commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

On iOS 26+, Apple introduced the liquid glass design language with translucent system chrome (tab bars, toolbars, navigation bars). For scroll views to properly adjust their content to account for this translucent chrome, contentInsetAdjustmentBehavior should default to UIScrollViewContentInsetAdjustmentScrollableAxes instead of UIScrollViewContentInsetAdjustmentNever.

This PR upgrades the default on iOS 26+ when UIDesignRequiresCompatibility is not YES in Info.plist, in three places:

  • RCTEnhancedScrollView.mm initWithFrame: — defaults to scrollableAxes instead of never
  • RCTScrollViewComponentView.mm updateProps: — upgrades incoming JS default never to scrollableAxes
  • RCTScrollViewComponentView.mm prepareForRecycle — resets to scrollableAxes instead of never

Behavior Matrix

iOS Version UIDesignRequiresCompatibility Default Behavior
< 26 any never (unchanged)
26+ YES never (unchanged)
26+ NO or absent scrollableAxes (new)

Apps that explicitly set contentInsetAdjustmentBehavior to "automatic", "always", or "scrollableAxes" from JS are unaffected.

Changelog:

[IOS] [FIXED] - Default contentInsetAdjustmentBehavior to scrollableAxes on iOS 26+ for liquid glass translucent chrome support

Test Plan:

  1. Create a React Native app targeting iOS 26 simulator
  2. Remove UIDesignRequiresCompatibility from Info.plist (or set to NO)
  3. Add a FlatList inside a screen with a translucent bottom tab bar (drawBehind: true)
  4. Before this PR: last items hidden behind the tab bar with no automatic inset
  5. After this PR: scroll view automatically adjusts content inset for the translucent tab bar
  6. Verify iOS 15–18 behavior is unchanged (never default)
  7. Verify UIDesignRequiresCompatibility = YES keeps never default on iOS 26+

Fixes #57299

… iOS 26+

On iOS 26+, Apple introduced the liquid glass design language with
translucent system chrome (tab bars, toolbars). Scroll views need
contentInsetAdjustmentBehavior set to scrollableAxes to automatically
adjust content insets for this translucent chrome.

This change upgrades the default from "never" to "scrollableAxes" on
iOS 26+ when UIDesignRequiresCompatibility is not YES, affecting:

- RCTEnhancedScrollView initWithFrame:
- RCTScrollViewComponentView updateProps: (upgrades JS default "never")
- RCTScrollViewComponentView prepareForRecycle

Apps that explicitly set contentInsetAdjustmentBehavior to "automatic",
"always", or "scrollableAxes" from JS are unaffected. Apps with
UIDesignRequiresCompatibility=YES retain the current "never" default.

Fixes react#57299

Co-authored-by: Cursor <cursoragent@cursor.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 21, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jun 21, 2026
auto contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior;
if (@available(iOS 26, *)) {
NSNumber *compat = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
if (!compat.boolValue && contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On iOS 26 without UIDesignRequiresCompatibility, this upgrades Never to ScrollableAxes whenever the value is Never. The prop arrives as Never both for the RN default and when a developer explicitly sets contentInsetAdjustmentBehavior="never", so this overrides an explicit "never" too.

The effect is that there is no per-view way to keep never on iOS 26 now, just the app-wide UIDesignRequiresCompatibility opt-out.

Is overriding an explicit "never" intended? If someone wants no inset adjustment on a specific scroll view on iOS 26, how would they express it? The prop does not seem to carry whether the value was explicit, so maybe there is no clean per-view fix. Wanted to check the intent.

@IsaacIsrael IsaacIsrael Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, you're right. contentInsetAdjustmentBehavior is a plain enum that defaults to Never, and the parser maps an explicit "never" to that same Never, so by the time it reaches updateProps, there's no way to tell "developer set never" from "developer set nothing". So yeah, the current patch overrides an explicit never too, which isn't what we want.

I'll change the approach: make contentInsetAdjustmentBehavior a std::optional<ContentInsetAdjustmentBehavior> (nullopt = not set by the developer). The iOS 26 scrollableAxes default then only kicks in when it's nullopt, and an explicit "never" is preserved per view. maintainVisibleContentPosition in the same props struct is already optional, so this keeps the pattern consistent.

Let me push that and I'll re-request your review.

…ve explicit never

Addresses review feedback: the prop was a non-optional enum defaulting to
Never, so an explicit JS "never" was indistinguishable from the default and
got upgraded to scrollableAxes on iOS 26 too.

Make contentInsetAdjustmentBehavior a std::optional<ContentInsetAdjustmentBehavior>
(nullopt = not set from JS). The iOS 26 liquid-glass default (scrollableAxes,
unless UIDesignRequiresCompatibility) is applied only when the prop is unset,
via a shared RCTDefaultContentInsetAdjustmentBehavior() helper used in
initWithFrame:, updateProps:, and prepareForRecycle. An explicit value from
JS - including "never" - is always honored, restoring a per-view opt-out.

Co-authored-by: Cursor <cursoragent@cursor.com>
…havior

Co-authored-by: Cursor <cursoragent@cursor.com>
@meta-codesync

meta-codesync Bot commented Jun 24, 2026

Copy link
Copy Markdown

@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D109568371.

@fabriziocucci

fabriziocucci commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Sorry @IsaacIsrael, I might have steered you in the wrong direction with the comment above.

I bootstrapped a small repro on an iOS 26.2 simulator to check the common case: a full height ScrollView with contentContainerStyle={{paddingTop: insets.top}} from useSafeAreaInsets (the usual way to inset scroll content now that core SafeAreaView is deprecated). Same screen, two columns, only contentInsetAdjustmentBehavior differs.

With never the content sits right below the status bar (just the manual padding). With scrollableAxes iOS adds its own top inset on top of the manual padding, so the first row is pushed down by roughly another safe-area height. So for apps that already pad their scroll content, flipping the default to scrollableAxes on iOS 26 double-insets them with no code change on their side. Screenshot below.

rnr_57300_doubleinset

It does not regress every layout. If the ScrollView sits inside a parent that is already padded so it does not reach the top edge, scrollableAxes finds no overlap and is a no-op. But the full-bleed scroll view plus padded content container is common enough to worry about.

The thing I keep coming back to: any app that wants this can already set contentInsetAdjustmentBehavior="scrollableAxes" today. So the only thing the PR really delivers is making it the default, which is also the part that double-insets the apps above. The value and the risk are the same change.

So my vote would be to keep never as the default for now and document the scrollableAxes opt-in for the liquid glass case. The opt-in already exists and is non-breaking, whereas flipping the default regresses the apps above.

I'll double-check this internally.

@IsaacIsrael

IsaacIsrael commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Hey @fabriziocucci, thanks for the feedback. No worries at all, I understand the issue, and your suggestion makes perfect sense.

But if we use contentInsetAdjustmentBehavior="automatic", would we get the same behavior?

The only real benefit I see from this PR is the convenience of not having to set contentInsetAdjustmentBehavior throughout the codebase. But could be also achieved in other ways.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS 26: default contentInsetAdjustmentBehavior should be scrollableAxes for liquid glass

2 participants