skip to content

In React Native, why can text in a custom font with the same fontSize and lineHeight sit at different heights on Android and iOS?

level: seniorimportance: nice to knowfreq 22%

answer

  1. vertical metrics come from the font file
  2. Android includeFontPadding defaults to true
  3. Android splits extra leading above and below
  4. iOS fixes line height and offsets baseline
  5. inspect lines with onTextLayout

basics

~20 s

Each platform builds the line box from the font's own ascent and descent differently. Android adds font padding by default (includeFontPadding: true) and splits lineHeight's extra space above and below; iOS pins the line height and shifts the baseline.

solid answer

~50 s

Text height in React Native comes from native text layout, and both platforms start from the **vertical metrics stored in the font file**, which custom fonts often set unevenly. Android's `Text` has `includeFontPadding`, default `true`, which adds extra space for tall ascenders and deep descenders, so a label centered in a button looks low or high. With `lineHeight`, Android spreads the difference between it and the font's ascent plus descent half above and half below, while iOS fixes the paragraph's line height and adds a baseline offset of half the difference to center the glyphs. When `lineHeight` is smaller than the font's natural height, iOS skips that offset and both platforms can clip. I set `includeFontPadding: false` with `textAlignVertical: 'center'` on Android, choose `lineHeight` at or above the natural height, and check the lines from `onTextLayout`.

code

tsx · 26 lines
tsx
import { Platform, StyleSheet, Text } from 'react-native';

export function SizeChip({ size }: { size: string }) {
  return (
    <Text
      style={styles.chip}
      onTextLayout={(e) => {
        const [line] = e.nativeEvent.lines;
        if (__DEV__ && line) console.log(line.height, line.ascender, line.descender, line.capHeight);
      }}>
      {size}
    </Text>
  );
}

const styles = StyleSheet.create({
  chip: {
    fontFamily: 'BrandSans-Medium',
    fontSize: 14,
    lineHeight: 20,
    ...Platform.select({
      android: { includeFontPadding: false, textAlignVertical: 'center' as const },
      default: {},
    }),
  },
});

go deeper

for a junior

Recall that Android Text has includeFontPadding, true by default, and that lineHeight behaves slightly differently on iOS and Android.

for a middle

Explain that native text layout measures from the font's ascent and descent, and describe how each platform distributes lineHeight's extra space.

for a senior

Diagnose off-center labels with borders and onTextLayout metrics, fix shared styles rather than components, and recognize when the font file's metrics are the real defect.

for a principal

Push the fix upstream: agree text metrics and line-height rules with the design system so product teams never hand-tune padding per platform.

## Where text height comes from In React Native, Yoga does not measure text itself. A `Text` node is a leaf whose size is asked of the **platform's text layout engine**, using the actual typeface, size and paragraph settings. That engine starts from the font file's **vertical metrics**: the ascent above the baseline, the descent below it, and the gap between lines. System fonts have well-balanced metrics. Brand fonts from a type foundry often do not: a large ascent value reserved for accented capitals, or a descent that differs between the formats you were given, is common. Those numbers, not the visible glyphs, decide where text sits in its box, and each platform turns them into a box differently. The measured height is not cosmetic. Yoga lays out siblings and parents from it, so a few extra pixels of font padding on Android change the height of every product card in a grid, push a price below a fold, or break any fixed row height that was computed by hand from the iOS rendering. ## Android: font padding and split leading Two React Native behaviours matter on Android. - **`includeFontPadding`**, an Android-only `Text` style, **defaults to `true`**. It adds extra space above and below the line for tall ascenders and deep descenders. With a custom font whose metrics are generous, this makes a single-line label look pushed down or up when it is centered in a button or a badge. React Native's docs recommend `includeFontPadding: false` together with **`textAlignVertical: 'center'`** for the best vertical centering. - **`lineHeight`** is implemented with a span that follows web-like rules: it computes the leading as `lineHeight` minus (ascent + descent) and adds **half above the first line's ascent and half below the last line's descent**. If `lineHeight` is smaller than the font's own ascent plus descent, the leading is negative, the box shrinks from both sides, and glyphs can be clipped. ## iOS: fixed line height and a baseline offset On iOS, React Native sets the paragraph's minimum and maximum line height to the `lineHeight` value, so every line box is exactly that tall. Glyphs are not centered in such a box on their own, so React Native adds a **baseline offset of half the difference** between `lineHeight` and the font's natural line height, which recenters the glyphs. In React Native 0.87, when `lineHeight` is **smaller** than the font's natural line height, that adjustment is skipped by default, so the glyphs are no longer centered and can be clipped. | | Android | iOS | |---|---|---| | Extra padding by default | yes, `includeFontPadding: true` | no | | How `lineHeight` adds space | half above, half below | fixed box plus baseline offset | | `lineHeight` below natural height | box shrinks both sides, may clip | no recentering, may clip | | Android-only knobs | `includeFontPadding`, `textAlignVertical` | none of these | Because the input metrics are the same but the algorithms differ, the same style can put a brand font's cap height a pixel or two apart, which is exactly what a designer notices on a price tag or a size chip. ## Diagnosing it 1. Put a temporary border or background on the `Text` so you can see its measured box, not just the glyphs. 2. Log `onTextLayout`. Its `lines` array reports each line's `x`, `y`, `width` and `height`, and the font's `ascender`, `descender`, `capHeight` and `xHeight`, so you can compare the metrics each platform actually used. 3. Compare against a system font at the same size. If the system font centers and the brand font does not, the font's metrics are the cause, not your layout. 4. Make sure the font was loaded before layout. Text measured with a fallback font and not re-measured gives misleading numbers. ## Fixes that hold up - On Android, set `includeFontPadding: false` and `textAlignVertical: 'center'` in the shared text styles for single-line labels. - Choose a `lineHeight` at or above the font's natural line height, which `onTextLayout` lets you read, and set it explicitly in shared styles rather than per component. - Keep these choices in the typography module so every screen inherits the same fix, and add a platform-specific nudge only after measuring. - If the metrics in the font file are simply wrong, the cleanest fix is a corrected font build with balanced vertical metrics, rather than per-component padding hacks.

  • Why does React Native's includeFontPadding: false alone sometimes still leave an Android label off-center?
    Removing font padding only drops the extra space Android reserves beyond ascent and descent. If the font's own ascent and descent are unbalanced, glyphs still sit off-center in the box. React Native's docs pair it with `textAlignVertical: 'center'`, and a font with bad vertical metrics may need a corrected build.
  • How can onTextLayout help settle a React Native text-alignment dispute between iOS and Android?
    `onTextLayout` delivers a `lines` array with each line's box plus the font's `ascender`, `descender`, `capHeight` and `xHeight` as the platform measured them. Logging it for the same string on both devices shows whether the fonts' metrics differ or the box does, which tells you whether to fix padding, `lineHeight` or the font file.

saying these in an interview costs you the question

  • React Native computes text height in Yoga, identically on both platforms.
  • lineHeight adds space only between lines, never above the first one.
  • includeFontPadding exists on iOS as well and defaults to false there.
  • A lineHeight smaller than the font size is always safe and never clips.
  • If the glyphs look centered in the design tool they will be centered on device.