Skip to content

fix(fabric): force overlay scrollbar style in ScrollView#2907

Open
Saadnajmi wants to merge 1 commit intomainfrom
scrollbar-overlay-only
Open

fix(fabric): force overlay scrollbar style in ScrollView#2907
Saadnajmi wants to merge 1 commit intomainfrom
scrollbar-overlay-only

Conversation

@Saadnajmi
Copy link
Copy Markdown
Collaborator

@Saadnajmi Saadnajmi commented Apr 9, 2026

Summary

  • Force NSScrollerStyleOverlay on Fabric ScrollViews to fix layout overflow on first render
  • Remove autoresizingMask from documentView to prevent AppKit frame corruption
  • Fix scrollbar click hit testing (check NSScroller before content subviews)
  • Re-force overlay style when the system "Show scroll bars" preference changes at runtime

Why force overlay instead of supporting legacy scrollbars?

  1. Every other platform uses overlay scrollbars. iOS, Android, and web all render scrollbar indicators that float above content. macOS is the only platform where scrollbars can sit inside the frame and reduce the visible content area. Forcing overlay aligns macOS behavior with every other React Native platform.

  2. Supporting legacy scrollbars required invasive changes to ReactCommon. The alternative approach required adding cached atomic values, custom shadow node constructors, and padding adjustments in the Yoga layout pass at the ScrollViewShadowNode C++ layer — a significant cross-platform change to support a single-platform edge case.

  3. Apple themselves call non-overlay scrollbars "legacy." The API is literally NSScrollerStyleLegacy. Mac Catalyst doesn't even respect this system preference (it always uses overlay). SwiftUI does respect it, but SwiftUI also has the advantage of proposing clip view size to children — something React Native's layout system doesn't do.

Test plan

  • Open RNTester on macOS with "Show scroll bars: Always" in System Settings
  • Verify ScrollView content does not overflow on first render
  • Verify scrollbars appear as overlay style (thin, semi-transparent)
  • Verify switching system preference at runtime doesn't bring back legacy scrollbars
  • Verify scrollbar clicks work (drag the scrollbar thumb)
  • Verify window resize doesn't cause content overflow
  • Test with "Show scroll bars: When scrolling" (default) — unchanged behavior

Fixes #2857

🤖 Generated with Claude Code

@Saadnajmi Saadnajmi requested a review from a team as a code owner April 9, 2026 21:05
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

⚠️ No Changeset found

Latest commit: ef37f10

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Saadnajmi Saadnajmi changed the title fix(fabric,macos): force overlay scrollbar style in ScrollView fix(fabric): force overlay scrollbar style in ScrollView Apr 9, 2026
@Saadnajmi
Copy link
Copy Markdown
Collaborator Author

/backport 0.81-stable

@microsoft-react-native-sdk
Copy link
Copy Markdown

Backport results

  • ⚠️ 0.81-stable : branch does not exist

Force NSScrollerStyleOverlay on Fabric ScrollViews to avoid layout
issues with legacy (always-visible) scrollbars. Legacy scrollbars sit
inside the NSScrollView frame and reduce the clip view's visible area,
which would require compensating padding in the Yoga shadow tree.
Overlay scrollers float above content, so no layout compensation is
needed.

Additional fixes:
- Remove autoresizingMask from documentView to prevent AppKit frame
  corruption during tile/resize
- Remove the layoutSubviews workaround (no longer needed without
  autoresizingMask)
- Add [_scrollView tile] after content size updates to re-evaluate
  scroller visibility
- Fix hit testing: check NSScroller before content subviews so
  scrollbar clicks aren't swallowed by full-width content views

Fixes #2857

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines -160 to 170
_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// Force overlay scrollbar style. Overlay scrollers float above content and
// don't reduce the clip view's visible area, so no layout compensation is
// needed. Legacy (always-visible) scrollbars sit inside the frame and would
// require padding adjustments in the Yoga shadow tree — avoiding that
// complexity is the main motivation for forcing overlay style.
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
// Do NOT set autoresizingMask on the documentView. AppKit's autoresizing
// corrupts the documentView frame during tile/resize (adding the clip view's
// size delta to the container, inflating it beyond the actual content size).
// React manages the documentView frame directly via updateState:.
[_scrollView setDocumentView:_containerView];
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We can probably minimize this comment

Comment on lines +199 to +202
// The user changed the system "Show scroll bars" preference. Re-force
// overlay style so legacy scrollbars don't appear and cause layout issues.
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
[_scrollView tile];
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Likewise with this comment

Comment on lines +557 to +560
#if TARGET_OS_OSX // [macOS
// Force the scroll view to re-evaluate which scrollers should be visible.
[_scrollView tile];
#endif // macOS]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Are we sure this isn't called as a side effect of setting content size or anything?

Comment on lines +588 to +600
#if TARGET_OS_OSX // [macOS
// Check if the hit lands on a scrollbar (NSScroller) BEFORE checking content
// subviews. Scrollers are subviews of the NSScrollView, not the documentView
// (_containerView). They must be checked first because content views typically
// fill the entire visible area and would otherwise swallow scroller clicks.
if (isPointInside) {
NSPoint scrollViewPoint = [_scrollView convertPoint:point fromView:self];
NSView *scrollViewHit = [_scrollView hitTest:scrollViewPoint];
if ([scrollViewHit isKindOfClass:[NSScroller class]]) {
return (RCTPlatformView *)scrollViewHit;
}
}
#endif // macOS]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Double check this is necessary

selector:@selector(scrollViewDocumentViewBoundsDidChange:)
name:NSViewBoundsDidChangeNotification
object:_scrollView.contentView]; // NSClipView
// Re-force overlay style if the user changes the system "Show scroll bars" preference
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Don't need this comment

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text inside the ScrollView has massive vertical and horizontal overflow on the first render

1 participant