Description
On iOS with the New Architecture, a <Text> that wraps to two or more lines draws one line short (the last line is laid out as a clipped single line) when its box crosses y = 2048 in the scroll content on a 3x screen. It depends on the box's position, not on the text.
Two things combine:
- Yoga rounds the height one float step short.
roundLayoutResultsToPixelGrid computes a node's height as round(absoluteBottom) − round(absoluteTop), and roundValueToPixelGrid returns float, so the subtraction is done in float. With the top below 2048 and the bottom above, the two edges are quantized with different steps (2⁻¹³ vs 2⁻¹²) and the result can be 43.9998779296875 instead of 44 for two lines of lineHeight: 22. onLayout reports exactly that height.
- TextKit then drops the last line.
RCTParagraphComponentView passes _layoutMetrics.getContentFrame() straight to RCTTextLayoutManager as the NSTextContainer size, for drawing and for onTextLayout. Two lines need 44.0, so the second line "doesn't fit"; RCTTextLayoutManager sets textContainer.lineBreakMode = NSLineBreakByClipping when numberOfLines is unset, so the last visible line is laid out without wrapping and clipped at the edge. onTextLayout reports 1 line whose width equals the container width and whose text is the whole paragraph. With numberOfLines set it shows the same single line with "…".
Measurement is unaffected (it measures with an unbounded height), so the row reserves the right space; only the draw is short. Any wrapped text with an explicit lineHeight whose box straddles 2048 (or 4096, …) in the content can hit it. We hit it in a production Expo SDK 57 app on an iPhone 16 Pro, in a list far down a screen.
Fix for part 1 proposed in Yoga: react/yoga#2037 (round in double, subtract, then narrow to float). Part 2 could be hardened separately, e.g. by rounding the container height up to the pixel grid before handing it to TextKit.
Steps to reproduce
- Run the
RNTesterPlayground.js from the reproducer link in RNTester on a 3x iOS simulator or device (e.g. iPhone 16 Pro), New Architecture.
- It renders 12 two-line
<Text>s positioned absolutely at tops 1896 / 1940 / 1984 / 2028 (+0, +⅓, +⅔) inside a 2300pt-tall view, and scrolls to them.
- The texts whose box crosses y = 2048 on a fraction (here "top 2028+1/3" and "top 2028+2/3") show only their first line. The console logs their
onLayout height 43.9998779296875 and onTextLayout line count 1. The others show "second line" and log nothing.
Expected: every box shows both lines; a box measured at 44 is laid out at 44.
React Native Version
0.86.3 (also present on main: ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp and RCTParagraphComponentView.mm are unchanged in the relevant lines)
Affected Platforms
Runtime - iOS, Build - MacOS
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
System:
OS: macOS 26.4
CPU: (14) arm64 Apple M4 Max
Memory: 424.95 MB / 36.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.22.2
path: /opt/homebrew/bin/node
Yarn:
version: 1.22.22
path: /opt/homebrew/bin/yarn
npm:
version: 10.9.7
path: /opt/homebrew/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.17.0
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.4
- iOS 26.4
- macOS 26.4
- tvOS 26.4
- visionOS 26.4
- watchOS 26.4
Android SDK: Not Found
IDEs:
Android Studio: Not Found
Xcode:
version: 26.4.1/17E202
path: /usr/bin/xcodebuild
Languages:
Java: Not Found
npmPackages:
react-native: 0.86.3 (Expo SDK 57, expo-dev-client, New Architecture)
Tested on the iOS 26.4 simulator, iPhone 16 Pro (3x), and seen on a physical iPhone in production.
Stacktrace or Logs
box top=2028.3333333333333: 1 line(s)
box top=2028.3333333333333: height 43.9998779296875
box top=2028.6666666666667: 1 line(s)
box top=2028.6666666666667: height 43.9998779296875
From the production app, the same <Text> in two positions (same style, same string):
DBG layout speaker {"x":0,"y":23.67,"width":292,"height":43.9998779296875}
DBG lines speaker 1 [[0,0,292,22,"Give a talk at an AIE.F event. Ask the organizers for a slot."]]
DBG layout barter {"x":0,"y":23.67,"width":292,"height":44}
DBG lines barter 2 [[0,0,272,22,"Check in to an event at Barter Community "],[0,22,139,22,"Hub, where it started."]]
MANDATORY Reproducer
Pull request replacing RNTesterPlayground.js, as asked: #58972 (draft, not for merging). The same file as a gist: https://gist.github.com/ybelakov/78912e731669264e74d50add13f2a6be
Screenshots and Videos
See the screenshot in the comments: the two boxes crossing 2048 on a fraction have no "second line".
Workaround
A tiny bottom padding on text styles (paddingBottom: 0.01) keeps the box height fractional, so Yoga's text rounding rounds it up to the next pixel and the float error can't eat the last line.
Description
On iOS with the New Architecture, a
<Text>that wraps to two or more lines draws one line short (the last line is laid out as a clipped single line) when its box crosses y = 2048 in the scroll content on a 3x screen. It depends on the box's position, not on the text.Two things combine:
roundLayoutResultsToPixelGridcomputes a node's height asround(absoluteBottom) − round(absoluteTop), androundValueToPixelGridreturnsfloat, so the subtraction is done in float. With the top below 2048 and the bottom above, the two edges are quantized with different steps (2⁻¹³ vs 2⁻¹²) and the result can be43.9998779296875instead of44for two lines oflineHeight: 22.onLayoutreports exactly that height.RCTParagraphComponentViewpasses_layoutMetrics.getContentFrame()straight toRCTTextLayoutManageras theNSTextContainersize, for drawing and foronTextLayout. Two lines need 44.0, so the second line "doesn't fit";RCTTextLayoutManagersetstextContainer.lineBreakMode = NSLineBreakByClippingwhennumberOfLinesis unset, so the last visible line is laid out without wrapping and clipped at the edge.onTextLayoutreports 1 line whose width equals the container width and whose text is the whole paragraph. WithnumberOfLinesset it shows the same single line with "…".Measurement is unaffected (it measures with an unbounded height), so the row reserves the right space; only the draw is short. Any wrapped text with an explicit
lineHeightwhose box straddles 2048 (or 4096, …) in the content can hit it. We hit it in a production Expo SDK 57 app on an iPhone 16 Pro, in a list far down a screen.Fix for part 1 proposed in Yoga: react/yoga#2037 (round in double, subtract, then narrow to float). Part 2 could be hardened separately, e.g. by rounding the container height up to the pixel grid before handing it to TextKit.
Steps to reproduce
RNTesterPlayground.jsfrom the reproducer link in RNTester on a 3x iOS simulator or device (e.g. iPhone 16 Pro), New Architecture.<Text>s positioned absolutely at tops 1896 / 1940 / 1984 / 2028 (+0, +⅓, +⅔) inside a 2300pt-tall view, and scrolls to them.onLayoutheight43.9998779296875andonTextLayoutline count1. The others show "second line" and log nothing.Expected: every box shows both lines; a box measured at 44 is laid out at 44.
React Native Version
0.86.3 (also present on
main:ReactCommon/yoga/yoga/algorithm/PixelGrid.cppandRCTParagraphComponentView.mmare unchanged in the relevant lines)Affected Platforms
Runtime - iOS, Build - MacOS
Areas
Fabric - The New Renderer
Output of
npx @react-native-community/cli infoTested on the iOS 26.4 simulator, iPhone 16 Pro (3x), and seen on a physical iPhone in production.
Stacktrace or Logs
From the production app, the same
<Text>in two positions (same style, same string):MANDATORY Reproducer
Pull request replacing
RNTesterPlayground.js, as asked: #58972 (draft, not for merging). The same file as a gist: https://gist.github.com/ybelakov/78912e731669264e74d50add13f2a6beScreenshots and Videos
See the screenshot in the comments: the two boxes crossing 2048 on a fraction have no "second line".
Workaround
A tiny bottom padding on text styles (
paddingBottom: 0.01) keeps the box height fractional, so Yoga's text rounding rounds it up to the next pixel and the float error can't eat the last line.