Skip to content

fix(scrollview): ScrollView layout with legacy (always-visible) scrollbars#2883

Draft
Saadnajmi wants to merge 2 commits intomainfrom
scrollbar-insets
Draft

fix(scrollview): ScrollView layout with legacy (always-visible) scrollbars#2883
Saadnajmi wants to merge 2 commits intomainfrom
scrollbar-insets

Conversation

@Saadnajmi
Copy link
Copy Markdown
Collaborator

@Saadnajmi Saadnajmi commented Apr 2, 2026

Summary

On macOS, legacy (always-visible) scrollbars sit inside the NSScrollView frame and reduce the NSClipView's visible area. React's layout system measures ScrollView children against the full frame, causing content to overflow behind the scrollbar.

Both Fabric and Paper now use the same approach: apply paddingEnd on the ScrollView's shadow node equal to the legacy scrollbar width. This reduces the available layout space for children to match the actual visible area — the same approach SwiftUI uses when proposing sizes to ScrollView children.

Fabric (commit 1)

  • Following the pattern from Fix Switch layout with iOS26 (#53067) facebook/react-native#53247, the scrollbar width is read synchronously during the Yoga layout pass via a cached std::atomic — no state round-trip, so the first layout is immediately correct
  • ScrollViewShadowNode::setSystemScrollbarWidth() is called from +[RCTScrollViewComponentView initialize] at class load time
  • applyScrollbarPadding() reads the atomic value directly during construction
  • Removes autoresizingMask from documentView (prevents AppKit frame corruption on first render)
  • Moves NSScroller hit test check before subview loop (fixes scrollbar click handling)
  • Observes NSPreferredScrollerStyleDidChangeNotification for runtime changes

Paper (commit 2)

  • New RCTScrollViewShadowView (defined inline in RCTScrollViewManager.m) applies paddingEnd in layoutWithMetrics: using thread-safe +[NSScroller preferredScrollerStyle] class methods
  • Eliminates the RCTScrollContentLocalData round-trip mechanism entirely (files deleted)
  • RCTScrollContentShadowView reverted to stock (only RTL fix remains)
  • preferredScrollerStyleDidChange: in RCTScrollView re-tiles and triggers content size update

Test plan

  • Launch RNTester on macOS with "Show scroll bars: Always" — no horizontal scrollbar, content fits within clip view
  • Switch to "When scrolling" (overlay) — layout updates, no extra gap
  • Switch back to "Always" — layout updates correctly
  • Click on legacy scrollbar — hit testing works, scrollbar responds
  • Click on overlay scrollbar — hit testing works
  • Resize window — no horizontal overflow
  • Test with both Paper (old arch) and Fabric (new arch)

🤖 Generated with Claude Code

@Saadnajmi Saadnajmi requested a review from a team as a code owner April 2, 2026 17:50
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 2, 2026

⚠️ No Changeset found

Latest commit: fe5efee

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.

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 marked this pull request as draft April 2, 2026 17:54
@Saadnajmi Saadnajmi changed the title [macOS] Fix ScrollView layout with legacy (always-visible) scrollbars fix(scrollview): ScrollView layout with legacy (always-visible) scrollbars Apr 2, 2026
… (Fabric)

On macOS, legacy scrollbars sit inside the NSScrollView frame and reduce
the NSClipView's visible area. React's layout system measures children
against the full frame, causing content to overflow behind the scrollbar.

Following the pattern from facebook#53247 (Switch), this fix
reads the scrollbar width synchronously during the Yoga layout pass via a
cached atomic value — no state round-trip, so the first layout is
immediately correct.

Key changes:
- Add ScrollViewShadowNode::setSystemScrollbarWidth() / getSystemScrollbarWidth()
  backed by a static atomic, called from native code at class load time
- applyScrollbarPadding() reads the cached value directly (not from state)
- Remove scrollbarTrailingWidth/scrollbarBottomHeight from ScrollViewState
- Remove autoresizingMask from documentView (prevents AppKit frame corruption)
- Move NSScroller hit test check before subview loop (fixes scrollbar clicks)
- Observe NSPreferredScrollerStyleDidChangeNotification for runtime changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Saadnajmi Saadnajmi force-pushed the scrollbar-insets branch 2 times, most recently from 5614e2d to 836e42d Compare April 2, 2026 20:34
Apply paddingEnd on the ScrollView's shadow node to account for legacy
scrollbar width, matching the Fabric approach. A new RCTScrollViewShadowView
(defined inline in RCTScrollViewManager.m) reads +[NSScroller
preferredScrollerStyle] and +scrollerWidthForControlSize: directly
during layoutWithMetrics: — these are thread-safe class methods, so no
localData round-trip is needed.

Key changes:
- Add RCTScrollViewShadowView with paddingEnd for scrollbar width
- Revert RCTScrollContentShadowView to stock (only RTL fix remains)
- Delete RCTScrollContentLocalData files (no longer used)
- Update preferredScrollerStyleDidChange: in RCTScrollView to re-tile
  and trigger content size update on system preference changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant