Environment
- **react-native -v:** 18.0.0
- **npm ls react-native-macos:** react-native-macos@0.79.4
- **node -v:** v18.19.0
- **npm -v:** 10.2.3
- **yarn --version:** 1.22.10
- **xcodebuild -version:** Xcode 16.0 (Build version 16A242d)
- **react-native-macos:** 0.79.4
- **Architecture:** Fabric (New Architecture)
- **macOS:** 14.x+ (Sonoma)
Steps to reproduce the bug
- Create a multiline
TextInput with a fixed height in a Fabric-enabled react-native-macos app:
<View style={{ height: 200, borderWidth: 1 }}>
<TextInput
multiline
scrollEnabled={true}
style={{ flex: 1 }}
defaultValue="Line 1\nLine 2\nLine 3\n..." // enough text to overflow 200px
/>
</View>
- Type or paste enough text to overflow the 200px visible area
- Try to scroll within the TextInput to see overflowed text
Expected Behavior
The multiline TextInput should scroll vertically when text content exceeds the fixed height, just as it does on iOS and in the Paper (Old Architecture) implementation on macOS. The vertical scrollbar should appear and the user should be able to scroll through all text content.
Actual Behavior
The text overflows but no scrolling is possible. The vertical scrollbar never appears. Text below the visible area is clipped and inaccessible. The scrollEnabled={true} prop has no effect.
Additionally, scrollCursorIntoView is unimplemented on macOS (has a // TODO comment at line ~959 of RCTTextInputComponentView.mm), so the cursor does not auto-scroll into view when typing beyond the visible area.
Root Cause Analysis
The issue is in RCTTextInputComponentView.mm. The _setupScrollViewForMultilineTextView method correctly creates an NSScrollView (RCTUIScrollView) wrapper with an RCTClipView and embeds the NSTextView (RCTUITextView) as the document view. However, the NSTextView (document view) never expands beyond the NSScrollView's visible bounds, so NSScrollView sees no overflow and never enables scrolling.
The problem in updateLayoutMetrics:oldLayoutMetrics:
if (_multiline && _scrollView) {
_scrollView.frame =
UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
_backedTextInputView.textContainerInset =
RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth);
}
Only the scroll view frame and text container insets are set. The NSTextView (document view) frame is never explicitly sized to match the actual text content height. Fabric's Yoga layout system constrains self.bounds based on the component's style height, and the NSTextView frame ends up matching the scroll view bounds exactly — meaning there is zero overflow for NSScrollView to scroll.
For NSScrollView to scroll, the document view (NSTextView) must be taller than the clip view. This is a fundamental AppKit requirement.
In contrast, the Paper implementation (RCTMultilineTextInputView.mm) doesn't have this issue because it lets AppKit's NSTextView autoresizing (verticallyResizable = YES) handle the document view sizing naturally, without Yoga-computed frame constraints.
Secondary issue: type casting
In _setupScrollViewForMultilineTextView, the code accesses NSTextView-specific properties (verticallyResizable, horizontallyResizable, textContainer) directly on _backedTextInputView, which is typed as NSView<RCTBackedTextInputViewProtocol> *. This compiles in the current code but would fail if strict type checking were enabled. These properties require a cast to NSTextView *.
Tertiary issue: scrollCursorIntoView unimplemented
- (void)scrollCursorIntoView
{
#if !TARGET_OS_OSX
// ... iOS implementation ...
#else // [macOS
// TODO
#endif // macOS]
}
Proposed Fix
1. updateLayoutMetrics:oldLayoutMetrics: — Force NSTextView to expand to content height
After setting the scroll view frame, compute the actual text layout height and set the NSTextView frame accordingly:
if (_multiline && _scrollView) {
CGRect scrollFrame =
UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
_scrollView.frame = scrollFrame;
_backedTextInputView.textContainerInset =
RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth);
if ([_backedTextInputView isKindOfClass:[NSTextView class]]) {
NSTextView *textView = (NSTextView *)_backedTextInputView;
CGFloat scrollWidth = scrollFrame.size.width;
textView.minSize = NSMakeSize(scrollWidth, scrollFrame.size.height);
textView.maxSize = NSMakeSize(scrollWidth, CGFLOAT_MAX);
textView.frame = NSMakeRect(0, 0, scrollWidth, scrollFrame.size.height);
NSLayoutManager *lm = textView.layoutManager;
NSTextContainer *tc = textView.textContainer;
[lm ensureLayoutForTextContainer:tc];
NSRect usedRect = [lm usedRectForTextContainer:tc];
NSSize insets = textView.textContainerInset;
CGFloat contentHeight = MAX(scrollFrame.size.height, usedRect.size.height + insets.height * 2);
NSRect textFrame = textView.frame;
textFrame.size.height = contentHeight;
textView.frame = textFrame;
}
}
2. _setupScrollViewForMultilineTextView — Add proper type cast
if ([_backedTextInputView isKindOfClass:[NSTextView class]]) {
NSTextView *textView = (NSTextView *)_backedTextInputView;
textView.verticallyResizable = YES;
textView.horizontallyResizable = YES;
textView.textContainer.containerSize = NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX);
textView.textContainer.widthTracksTextView = YES;
}
3. scrollCursorIntoView — Implement macOS version
#else // [macOS
NSRange selectedRange = [_backedTextInputView selectedRange];
[_backedTextInputView scrollRangeToVisible:selectedRange];
#endif // macOS]
Note on NSTextView.textContainerInset
On macOS, NSTextView.textContainerInset returns NSSize (not NSEdgeInsets as on iOS). The width component is the horizontal inset and height is the vertical inset.
Reproducible Demo
Minimal reproduction — any Fabric-enabled react-native-macos 0.79.4 app with:
import React from 'react';
import { View, TextInput } from 'react-native';
export default function App() {
return (
<View style={{ padding: 40 }}>
<View style={{ height: 200, borderWidth: 1, borderColor: '#ccc', borderRadius: 8 }}>
<TextInput
multiline
scrollEnabled={true}
style={{ flex: 1, padding: 10 }}
defaultValue={Array.from({ length: 50 }, (_, i) => `Line ${i + 1}: Some text content here`).join('\n')}
/>
</View>
</View>
);
}
Additional context
Additional context
- The Paper (Old Architecture) implementation in
RCTMultilineTextInputView.mm does not have this issue — scrolling works correctly because AppKit's native NSTextView autoresizing handles document view sizing without Yoga frame constraints.
RCTUIScrollView.scrollEnabled is a stored property with no behavioral effect on macOS — it never actually enables/disables scrolling on the NSScrollView. The actual scroll blocking is done via RCTClipView.constrainScrolling, which defaults to NO (scrolling allowed). So the scroll infrastructure is in place — the NSTextView just never grows beyond the visible area.
- We are using this fix in production via
patch-package. Happy to submit a PR if this approach is accepted.
Environment
Steps to reproduce the bug
TextInputwith a fixed height in a Fabric-enabled react-native-macos app:Expected Behavior
The multiline
TextInputshould scroll vertically when text content exceeds the fixed height, just as it does on iOS and in the Paper (Old Architecture) implementation on macOS. The vertical scrollbar should appear and the user should be able to scroll through all text content.Actual Behavior
The text overflows but no scrolling is possible. The vertical scrollbar never appears. Text below the visible area is clipped and inaccessible. The
scrollEnabled={true}prop has no effect.Additionally,
scrollCursorIntoViewis unimplemented on macOS (has a// TODOcomment at line ~959 ofRCTTextInputComponentView.mm), so the cursor does not auto-scroll into view when typing beyond the visible area.Root Cause Analysis
The issue is in
RCTTextInputComponentView.mm. The_setupScrollViewForMultilineTextViewmethod correctly creates anNSScrollView(RCTUIScrollView) wrapper with anRCTClipViewand embeds theNSTextView(RCTUITextView) as the document view. However, theNSTextView(document view) never expands beyond theNSScrollView's visible bounds, soNSScrollViewsees no overflow and never enables scrolling.The problem in
updateLayoutMetrics:oldLayoutMetrics:Only the scroll view frame and text container insets are set. The
NSTextView(document view) frame is never explicitly sized to match the actual text content height. Fabric's Yoga layout system constrainsself.boundsbased on the component's style height, and theNSTextViewframe ends up matching the scroll view bounds exactly — meaning there is zero overflow forNSScrollViewto scroll.For
NSScrollViewto scroll, the document view (NSTextView) must be taller than the clip view. This is a fundamental AppKit requirement.In contrast, the Paper implementation (
RCTMultilineTextInputView.mm) doesn't have this issue because it lets AppKit'sNSTextViewautoresizing (verticallyResizable = YES) handle the document view sizing naturally, without Yoga-computed frame constraints.Secondary issue: type casting
In
_setupScrollViewForMultilineTextView, the code accessesNSTextView-specific properties (verticallyResizable,horizontallyResizable,textContainer) directly on_backedTextInputView, which is typed asNSView<RCTBackedTextInputViewProtocol> *. This compiles in the current code but would fail if strict type checking were enabled. These properties require a cast toNSTextView *.Tertiary issue:
scrollCursorIntoViewunimplementedProposed Fix
1.
updateLayoutMetrics:oldLayoutMetrics:— Force NSTextView to expand to content heightAfter setting the scroll view frame, compute the actual text layout height and set the NSTextView frame accordingly:
2.
_setupScrollViewForMultilineTextView— Add proper type cast3.
scrollCursorIntoView— Implement macOS versionNote on
NSTextView.textContainerInsetOn macOS,
NSTextView.textContainerInsetreturnsNSSize(notNSEdgeInsetsas on iOS). Thewidthcomponent is the horizontal inset andheightis the vertical inset.Reproducible Demo
Minimal reproduction — any Fabric-enabled react-native-macos 0.79.4 app with:
Additional context
Additional context
RCTMultilineTextInputView.mmdoes not have this issue — scrolling works correctly because AppKit's nativeNSTextViewautoresizing handles document view sizing without Yoga frame constraints.RCTUIScrollView.scrollEnabledis a stored property with no behavioral effect on macOS — it never actually enables/disables scrolling on the NSScrollView. The actual scroll blocking is done viaRCTClipView.constrainScrolling, which defaults toNO(scrolling allowed). So the scroll infrastructure is in place — the NSTextView just never grows beyond the visible area.patch-package. Happy to submit a PR if this approach is accepted.