Skip to content

iOS: Text drops its last line when its box crosses y = 2048 on a 3x screen (Yoga float rounding) #58970

Description

@ybelakov

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:

  1. 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.
  2. 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

  1. Run the RNTesterPlayground.js from the reproducer link in RNTester on a 3x iOS simulator or device (e.g. iPhone 16 Pro), New Architecture.
  2. 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.
  3. 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.

Activity

  1. react-native-bot commented on Oct 9, 2026

    @react-native-bot
    Collaborator

    Warning

    Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:


    You can read more about about it on our website: How to report a bug.

  2. ybelakov commented on Oct 9, 2026

    @ybelakov
    Author
    Image
  3. ybelakov commented on Oct 9, 2026

    @ybelakov
    Author

    Reproducer as a pull request editing RNTesterPlayground.js: #58972 (draft, not for merging). On a 3x iOS simulator the boxes crossing y = 2048 on a fraction show only their first line and log a height of 43.9998779296875.

  4. added 3 commits that reference this issue on Oct 9, 2026
    e26248c
    ab5447b
    3299647
  5. ybelakov commented on Oct 9, 2026

    @ybelakov
    Author

    Fix for the React Native side (works with or without the Yoga fix): #58974. On a React Native built from source with that change, the reproducer draws all twelve boxes with both lines while Yoga still reports the 43.9998779296875pt box, and explicit short heights (30, 43.67) still clip as before.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: AttentionIssues where the author has responded to feedback.Needs: ReproThis issue could be improved with a clear list of steps to reproduce the issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions