iOS: a line containing an emoji sits ~2 pt too high in `<Text>` with a custom font and a fixed `lineHeight` (fallback font descent moves the baseline)
Los mantenedores suelen responder en 2 días
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 58/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- ios, objective-c, react-native
Línea de trabajo
Start with RCTTextLayoutManager.mm and RCTAttributedTextUtils.mm, especially the factory and baseline offset logic described in the issue. Reproduce the five-line Karla example on iOS and compare baseline spacing with the system-font block. Done means emoji lines keep even spacing at a fixed lineHeight, while lines without fallback glyphs remain unchanged; account for the issue’s proposed delegate behavior and attachment and last-line cases.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Description
On iOS, when a <Text> uses a custom (bundled) font and a fixed lineHeight, every line that
contains an emoji (or any other glyph that falls back to a different font) is drawn about 2 pt
higher than the lines around it: the gap above that line shrinks, the gap below grows. The system
font is not affected, see the reproducer. Android is not affected.
Measured with fontSize: 16, lineHeight: 22.4 (@3x): the baseline pitch of five consecutive
lines with an emoji on lines 2 and 4 is 57 / 70 / 57 / 69 px instead of four equal values.
Cause
- A fixed
lineHeightbecomesminimumLineHeight == maximumLineHeighton the paragraph style, so
TextKit clamps every line fragment to that height. - Before layout,
NSTextStoragefont-fixing replaces the font attribute of the emoji characters
with the fallback font (AppleColorEmoji). TextKit then places the line's baseline at
fragmentHeight − (largest descent among the fonts in the line's font attributes). Apple Color
Emoji's descent is larger than the text font's (0.39 em vs 0.25 em for the font we use), so the
baseline of that line moves up by the difference. RCTApplyBaselineOffset(RCTAttributedTextUtils.mm) centres the text with one
NSBaselineOffsetAttributeNamecomputed from the fonts that are attributes on the string. It
runs before the layout manager is attached and never sees the substituted font, so it cannot
compensate.enableIOSTextBaselineOffsetPerLinedoes not help for the same reason.
I verified the TextKit rule on device by logging the proposed baselineOffset in an
NSLayoutManagerDelegate: on 1,000+ lines without fallback glyphs the value equals
fragmentHeight − maxDescent(fontAttributes) to three decimals; on emoji lines it is exactly
descent(AppleColorEmoji) − descent(declaredFont) smaller.
Related: #47621 (emoji stretching lines when there is no fixed lineHeight), #44932 (an earlier
baseline regression, fixed in 0.75), #59549 (inline views on fallback-font runs). None of them
covers the fixed-lineHeight baseline shift.
Proposed fix
Give the NSLayoutManager created in RCTTextLayoutManager's factory
(_textStorageAndLayoutManagerWithAttributesString:paragraphAttributes:size:) a delegate that
implements layoutManager:shouldSetLineFragmentRect:lineFragmentUsedRect:baselineOffset:inTextContainer:forGlyphRange:
and, for paragraphs with a fixed line height, moves the baseline down by
maxDescent(laid-out fonts) − maxDescent(declared fonts) when that is > 0.5 pt. The declared fonts are
read from the original attributed string (which the factory already has as a parameter; the text
storage's attributes are font-fixed), with descents scaled to the laid-out size so
adjustsFontSizeToFit keeps working. Lines without fallback glyphs have a difference of exactly 0
and are untouched, so existing text renders pixel-identically. Lines containing attachments are
skipped. On the last line of a paragraph the emoji then overhangs the line box, which the paragraph
view would clip; growing that last fragment by the overhang (≈2 pt) avoids it.
The delegate must be set before [textStorage addLayoutManager:layoutManager]: TextKit lays out
synchronously at that point, and a delegate attached afterwards only takes effect after a second
layout.
I have this working as an app-side hook (a swizzle of the factory, because React Core is linked
prebuilt in our Expo app). Measured result on the same five lines: 64 / 63 / 63 / 63 px. Happy to
turn it into a PR against RCTTextLayoutManager.mm if you want it.
Steps to reproduce
- Render the snippet below on an iOS device or simulator.
- Look at the five lines of the first (Karla) block: line 2 and line 4 contain an emoji.
- Compare with the second block, which uses the system font.
Expected: five evenly spaced lines in both blocks (fixed lineHeight).
Actual: in the Karla block, lines 2 and 4 sit closer to the line above than to the line below,
by about 2 pt. The system-font block is even.
Reproducer
The effect needs a custom font: with the system font the emoji falls back through the font's own
cascade list and the line keeps its baseline. With a font that has no cascade list of its own (any
bundled or Google font, here Karla) NSTextStorage rewrites the emoji's font attribute to Apple
Color Emoji and the baseline moves. The snippet shows both side by side.
import { Text, View } from 'react-native';
import { useFonts, Karla_400Regular } from '@expo-google-fonts/karla';
const lines = [
'Line one without emoji, only text.',
'Line two with a pumpkin 🎃 in the middle.',
'Line three without emoji again.',
'Line four ends with a ghost 👻',
'Line five without emoji, the end.',
];
export default function App() {
const [loaded] = useFonts({ Karla_400Regular });
if (!loaded) return null;
return (
<View style={{ padding: 24, paddingTop: 80, gap: 32 }}>
<Text style={{ fontFamily: 'Karla_400Regular', fontSize: 16, lineHeight: 22.4 }}>
{lines.join('\n')}
</Text>
<Text style={{ fontSize: 16, lineHeight: 22.4 }}>{lines.join('\n')}</Text>
</View>
);
}
Snack: https://snack.expo.dev/@ralf-siewert-rg/privileged-yellow-bagel
React Native Version
0.83.10 (New Architecture). The relevant code is unchanged in 0.88.0
(RCTTextLayoutManager.mm factory and RCTApplyBaselineOffsetForRange compared by hand).
Affected Platforms
Runtime - iOS
Output of npx react-native info
System:
OS: macOS 26.6.2
CPU: (18) arm64 Apple M5 Pro
Memory: 83.47 MB / 24.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 26.4.0
path: /var/folders/1m/h9nkfgrx1c141zsvf_kb1zjc0000gn/T/yarn--1793892505781-0.6795164428725953/node
Yarn:
version: 1.22.22
path: /var/folders/1m/h9nkfgrx1c141zsvf_kb1zjc0000gn/T/yarn--1793892505781-0.6795164428725953/yarn
npm:
version: 12.1.0
path: /Users/ralfsiewert/.nvm/versions/node/v26.4.0/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Android SDK: Not Found
IDEs:
Android Studio: 2026.1 AI-261.23567.138.2611.15646644
Xcode:
version: 26.3/17C529
path: /usr/bin/xcodebuild
Languages:
Java:
version: 21.0.11
path: /usr/bin/javac
Ruby:
version: 4.0.5
path: /opt/homebrew/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.2.0
wanted: ^20.2.0
react:
installed: 19.2.0
wanted: 19.2.0
react-native:
installed: 0.83.10
wanted: 0.83.10
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
- Lenguaje dominante
- C++
- Estrellas
- 127k
- Forks
- 25.5k
- Merge medio
- 3 d 18 h
- PR fusionados (30 d)
- 188
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de react/react-native
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
react/react-native#59945 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
react/react-native#59944 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
react/react-native#59942 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
react/react-native#59939 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
react/react-native#59929 · 1 comentario ·
Los mantenedores suelen responder en 2 días
Todos los issues de react/react-native
Issues similares
-
test(tools): two testkit cache tests fail unless GOCACHE and GOMODCACHE are in the environmentAbiertobacklog
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
EchoTools/nevr-runtime#2385 ·
Los mantenedores suelen responder en 1 día
-
type:bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
tensorflow/tflite-micro#3995 ·
Los mantenedores suelen responder en 1 día
-
CPU update 1.31xAbiertospeed-report
Dificultad 1/5 Menos de una hora Aptitud para principiantes 75/100
mw00/project-maya#961 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
not for AI lovers
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
OSGeo/gdal#15515 · 1 comentario ·
Los mantenedores suelen responder en 1 día