Skip to content

Native <Heading>/<Paragraph> render invisible text — unitless lineHeight treated as pixels on React Native #17

@0xNeit

Description

@0xNeit

Summary

On React Native, <Heading> and <Paragraph> render invisible text — the
element occupies its layout height, but no glyphs are visible. The cause is a
unitless lineHeight value that is valid as a CSS ratio on web but is
interpreted as absolute pixels by React Native.

Environment

  • usemotif / @usemotif/react-native 1.0.0
  • Expo SDK 54, React Native 0.81.5, React 19.1, Hermes
  • iOS (iPhone 16 Pro simulator) — but the cause is platform-agnostic RN behaviour
  • Runtime path only (no compiler plugin)

Symptom

import { Heading, Text } from 'usemotif';

// Renders blank space — the heading is invisible.
<Heading level={4} color="$colors.text.default">Hello</Heading>

// Renders fine.
<Text fontSize="$xl" fontWeight="$bold" color="$colors.text.default">Hello</Text>

The <Heading> reserves vertical space but shows nothing, which reads as a
mystery gap above the next element.

Root cause

packages/react-native/src/typography.tsx:

export function Heading({
  level = 2,
  children,
  ...rest
}: HeadingProps): ReactElement {
  return (
    <Text
      fontSize={headingSize[level - 1]!}
      fontWeight="$bold"
      lineHeight={1.2} // <-- unitless: a web line-height ratio
      accessibilityRole="header"
      {...rest}
    >
      {children}
    </Text>
  );
}

Paragraph has the same issue with lineHeight={1.6}.

On the web, line-height: 1.2 is a unitless multiplier (1.2 × font-size). On
React Native, lineHeight is an absolute value in DIPs — there is no
unitless form. So the native Heading sets a line box 1.2px tall and clips
the glyphs to nothing. Text works because it sets no lineHeight at all.

This contradicts the file's own header comment, which states the native
primitives use "the same defaults as the web implementations so cross-platform
code looks the same" — the value is the same but its meaning is not.

Reproduction

  1. Fresh Expo app, install usemotif + @usemotif/tokens.
  2. Mount <ThemeProvider> and render <Heading level={4}>Hello</Heading>.
  3. The heading is invisible; an equivalent <Text> is not.

Confirmed workaround

Pass an explicit pixel lineHeight{...rest} spreads after the hard-coded
default, so it overrides:

<Heading level={4} lineHeight={26}>
  Hello
</Heading> // visible

Suggested fix

In the native typography primitives, resolve a unitless lineHeight against the
resolved fontSize (lineHeight × fontSize → px) before passing it to RN —
either inside Heading/Paragraph, or generically in the native style resolver
so any consumer passing a unitless lineHeight (a natural web habit) gets
correct cross-platform behaviour. The latter keeps the "cross-platform code
looks the same" promise intact.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical — ship-blocker

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions