Repository navigation
Text lineHeight is honoured on web but not on React Native — multi-line text lays out ~5% taller on iOS #441
Description
Summary
A Text with an explicitly authored lineHeight of 40 (font size 32) lays out at 40px per line in @rive-app/canvas, and at 42px per line in @rive-app/react-native on iOS — from the same .riv file, same text style, same font asset.
The authored value is read and applied on RN — a second test at lineHeight 60 confirms it changes the layout — but the applied pitch is larger than authored. The divergence at 40 is a clean 1.0500 ratio, measured, not estimated.
This breaks any layout that positions content by arithmetic on line height — for us, an odometer-style digit register that translates an 11-line strip to bring one digit into a clipping window.
Environment
| RN runtime | @rive-app/react-native 0.4.19 (Nitro) |
react-native-nitro-modules 0.35.10 |
|
| RiveRuntime 6.21.1 | |
| Web runtime | @rive-app/canvas 2.39.2 |
| Device | iOS Simulator, iPhone 17 Pro |
| Editor | Rive Desktop, beta 0.8.5581 |
Authored values
On the TextStylePaint (property keys as reported by the editor's MCP surface):
| property | key | value |
|---|---|---|
fontsize |
274 | 32.0 |
lineheight |
370 | 40.0 |
letterspacing |
390 | 0.96 |
fontassetid |
279 | an embedded font asset (not a system font; iscustomfont = false) |
On the Text node: sizingvalue = autoWidth, paragraphspacing = 0, verticaltrim = none, fitfrombaseline = true.
The text content is 11 lines: one blank line followed by 0–9. Its container is a fixed 20 x 440 box — 440 = 11 x 40, i.e. sized on the assumption that the authored line height is respected.
Expected
11 lines at lineHeight 40 occupy 440px on every runtime.
Actual
- Web: 440px. Correct.
- RN / iOS: ~462px, i.e. 42px per line.
Evidence
The strip is translated by -((d + 1) * 40) to bring digit d into a 40px clipping window. If the true pitch is 42 rather than 40, each digit lands low by (d + 1) * 40 * 0.05 — an error that grows linearly with the digit value.
Measured glyph tops from a device screenshot (xcrun simctl io … screenshot, luma threshold, 3x scale), same numeric value in two registers, one with a workaround applied and one without:
value 1907, digits 1 / 9 / 0 / 7
unmodified (control) glyph tops 796, 835, 790, 830 spread 45px
workaround applied glyph tops 790, 792, 790, 796 spread 6px
web reference spread equivalent to ~15px at the same scale
(natural round-vs-flat glyph variation; this is the floor)
The control's 45px spread matches the linear-error prediction; the workaround's 6px sits below the glyph-shape floor measured on the known-good web render.
Solving for the pitch ratio from the digit-4-vs-digit-0 gap (24px at 3x = 8 layout units, against a predicted 160(k−1)):
160(k − 1) = 8 -> k = 1.0500 -> rendered pitch = 42.00px
The same model predicts every value we rendered:
| value | predicted per-digit error (units) | observed |
|---|---|---|
| 404 | 4:10, 0:2, 4:10 | 0 appears raised, 4s flush |
| 7890 | 7:16, 8:18, 9:20, 0:2 | 7/8/9 badly low, 0 fine |
| 2607 | 2:6, 6:14, 0:2, 7:16 | 6 and 7 low, 2 and 0 near-flush |
| 1005 | 1:4, 0:2, 0:2, 5:12 | "100" flush, 5 low |
Second data point — lineHeight IS honoured, just not at the authored value
We re-ran with the authored lineHeight changed from 40 to 60 on a single strip, leaving the others untouched as controls, and rendered a value whose leading wheel shows digit 1.
Predicted displacement of that digit for each candidate mechanism (the strip is translated by -(d+1) * 40, so the error is (d+1) * (P - 40)):
| candidate | rendered pitch P | digit-1 displacement |
|---|---|---|
lineHeight ignored, font advance used |
42 | +4 units — digit stays centred in the 40-unit window |
additive, L + 2 |
62 | +44 units — digit leaves the window |
multiplicative, L x 1.05 |
63 | +46 units — digit leaves the window |
Observed: the digit was scrolled fully out of the window — only the edge of a neighbouring glyph remained visible, while the untouched control wheels stayed flush.
That rules out the font-fallback explanation. lineHeight is read and applied; the applied value is simply larger than authored — approximately 42 for an authored 40, and approximately 62–63 for an authored 60.
We could not separate additive (+2) from multiplicative (x1.05): they differ by ~6px at 3x, and at lineHeight 60 the glyph is already outside the clipping window, so there is no in-window measurement that distinguishes them. Both are consistent with every observation. We expect this is immediately obvious from your text-layout source.
Minimal repro
- A
Textwith 11 lines, font size 32,lineHeight40, inside a fixed 20 x 440 container. - Render the same
.rivin@rive-app/canvasand in@rive-app/react-native0.4.19 on iOS. - Measure the vertical distance between the first and last line's baselines. Web gives 400 (10 x 40); RN gives ~420.
Alternatively, translate the strip by -40 * n and observe that the n-th line is not centred in a 40px window on RN, with the offset growing with n.
Workaround (for anyone hitting this)
Force the renderer to fit the text into a known box rather than relying on the authored line height:
sizingvalue autoWidth -> fixed
width -> <container width>
height -> <lines * intended lineHeight>
overflowvalue visible -> fit
With overflow: fit, both runtimes scale the block into the fixed height, so the per-line pitch becomes correct by construction. On RN the glyphs render ~5% smaller as a result, which was an acceptable trade for us. Verified: web unchanged, RN corrected to within glyph-shape noise.
Why it matters beyond our case
lineHeight being platform-dependent silently breaks any design that does arithmetic on it — digit reels, tickers, virtualised lists, anything positioned by line index. There is no error and no warning; the content renders, just in the wrong place, and the error is proportional so small cases look fine and large ones look broken. It took us a full session to localise because every automated check we had asserts view-model values rather than rendered geometry.
Activity