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
Text with 11 lines, font size 32, lineHeight 40, inside a fixed 20 x 440 container.
- Render the same
.riv in @rive-app/canvas and in @rive-app/react-native 0.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.
Summary
A
Textwith an explicitly authoredlineHeightof 40 (font size 32) lays out at 40px per line in@rive-app/canvas, and at 42px per line in@rive-app/react-nativeon iOS — from the same.rivfile, same text style, same font asset.The authored value is read and applied on RN — a second test at
lineHeight60 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
@rive-app/react-native0.4.19 (Nitro)react-native-nitro-modules0.35.10@rive-app/canvas2.39.2Authored values
On the
TextStylePaint(property keys as reported by the editor's MCP surface):fontsizelineheightletterspacingfontassetidiscustomfont= false)On the
Textnode: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
lineHeight40 occupy 440px on every runtime.Actual
Evidence
The strip is translated by
-((d + 1) * 40)to bring digitdinto 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: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)):The same model predicts every value we rendered:
Second data point —
lineHeightIS honoured, just not at the authored valueWe re-ran with the authored
lineHeightchanged from 40 to 60 on a single strip, leaving the others untouched as controls, and rendered a value whose leading wheel shows digit1.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)):lineHeightignored, font advance usedL + 2L x 1.05Observed: 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.
lineHeightis 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
lineHeight60 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
Textwith 11 lines, font size 32,lineHeight40, inside a fixed 20 x 440 container..rivin@rive-app/canvasand in@rive-app/react-native0.4.19 on iOS.Alternatively, translate the strip by
-40 * nand observe that the n-th line is not centred in a 40px window on RN, with the offset growing withn.Workaround (for anyone hitting this)
Force the renderer to fit the text into a known box rather than relying on the authored line height:
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
lineHeightbeing 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.