DEV Community

Cover image for 🔠 Font Scaling in React Native — A Responsive Typography Solution
Onyeka Ikedinobi
Onyeka Ikedinobi

Posted on

🔠 Font Scaling in React Native — A Responsive Typography Solution

Let’s be real, hardcoding fontSize: 16 in your React Native app might look okay on your test device... but then your friend opens it on their monster-sized phone and it's barely readable. Or worse, your layout breaks.

🌟 The problem? Font sizes in React Native don’t scale by default.

In this article, I’ll walk you through how to fix that once and for all using:

  • A simple responsiveFontSize() utility,
  • A reusable custom <Text> component

Let’s make your typography look 🔥 on every screen.


💥 The Problem with Static Font Sizes

Take this simple line of code:

<Text style={{ fontSize: 16 }}>Hello world!</Text>
Enter fullscreen mode Exit fullscreen mode

On an iPhone 11? Great.

On an iPhone 16 Pro Max? It looks like a tiny label from 2012.

On a budget Android? It might get chopped off (chopped 🫢).

We’re designing for a huge range of screen sizes and pixel densities, and static font sizes just don’t cut it anymore.


✅ The Fix: Responsive Font Scaling

Here’s a lightweight utility that scales your fonts based on screen width—without making them too big or too small:

import { Dimensions, PixelRatio } from "react-native";

const { width: SCREEN_WIDTH } = Dimensions.get("window");

// Base width from iPhone 11
const BASE_WIDTH = 375;

const widthScale = SCREEN_WIDTH / BASE_WIDTH;

const moderateScale = (size: number, factor = 0.5) =>
  size + (widthScale * size - size) * factor;

export const responsiveFontSize = (fontSize: number, factor = 0.5) => {
  const newSize = moderateScale(fontSize, factor);
  return Math.round(PixelRatio.roundToNearestPixel(newSize));
};
Enter fullscreen mode Exit fullscreen mode

💡 Why not scale based on height too?
In most UI design systems, width-based scaling produces more consistent visual results across devices. It’s also more stable when handling orientation changes.


🧪 Visual Comparison — Static vs Responsive Fonts

Here’s how font sizes behave across two common devices using static vs responsive sizing.

iPhone 11 (375 × 812)

Static Font (fontSize: 16) Responsive Font (responsiveFontSize(16))
iPhone 11 Static iPhone 11 Responsive

iPhone 16 Pro Max (430 × 932)

Static Font (fontSize: 16) Responsive Font (responsiveFontSize(16))
iPhone 16 Static iPhone 16 Responsive

🌟 Takeaway: On larger screens, the static font looks awkwardly small. But with responsive sizing, everything just feels... right.


🤩 Make It Reusable: A Custom <Text /> Component

Let’s wrap this into a flexible Text component that your whole app can use:

import { forwardRef } from "react";
import { Text as DefaultText } from "react-native";
import { Colors, setOpacity } from "@/constants/colors";
import {
  Typography,
  FontWeight,
  TextSize,
} from "@/constants/typography";

type RNTextProps = DefaultText["props"];

export interface TextProps extends RNTextProps {
  size?: TextSize | number;
  weight?: FontWeight;
  dimRate?: `${number}%`;
  textTransform?: "none" | "capitalize" | "uppercase" | "lowercase";
}

export const Text = forwardRef<DefaultText, TextProps>((props, ref) => {
  const {
    size = "small",
    children,
    weight = "regular",
    textTransform = "none",
    dimRate,
    style,
    ...otherProps
  } = props;

  const getFontSize = () => {
    if (typeof size === "number") return size;
    return Typography.size[size] || Typography.size["small"];
  };

  const getLineHeight = () => {
    if (typeof size === "number") return size * 1.5;
    return Typography.lineHeight[size] || Typography.lineHeight["small"];
  };

  const getTextColor = () => {
    if (dimRate)
      return setOpacity(Colors.text, parseFloat(dimRate.replace("%", "")) / 100);
    return Colors.text;
  };

  const getFontFamily = () => {
    return Typography.fontFamily[weight];
  };

  return (
    <DefaultText
      style={[
        {
          fontSize: getFontSize(),
          fontFamily: getFontFamily(),
          color: getTextColor(),
          textTransform,
          lineHeight: getLineHeight(),
          flexShrink: 1,
        },
        style,
      ]}
      ref={ref}
      {...otherProps}
    >
      {children}
    </DefaultText>
  );
});

Text.displayName = "Text";
Enter fullscreen mode Exit fullscreen mode

🧪 Example Usage

<Text size="large" weight="bold" dimRate="80%">
  Responsive, readable, and beautiful!
</Text>
Enter fullscreen mode Exit fullscreen mode

Or if you want to use it manually:

<Text size={responsiveFontSize(18)}>
  This font adjusts to your screen size.
</Text>
Enter fullscreen mode Exit fullscreen mode

🗂 Bonus

Centralize Your Typography Config

To keep things clean and consistent, we’ve centralized font sizes, line heights, and font families inside a shared Typography config.

This config is already being used inside the custom component we built earlier — so all of your app's text elements can follow the same responsive rules without repeating logic.

export const Typography = {
    size: {
      small: responsiveFontSize(14),
      medium: responsiveFontSize(16),
      large: responsiveFontSize(20),
    },
    lineHeight: {
      small: responsiveFontSize(21),
      medium: responsiveFontSize(24),
      large: responsiveFontSize(30),
    },
    fontFamily: {
      regular: "Inter-Regular",
      bold: "Inter-Bold",
    },
};

export type FontWeight = keyof typeof Typography.headline.fontFamily;
export type TextSize = keyof typeof Typography.headline.size;
Enter fullscreen mode Exit fullscreen mode

🎨 Built-in Dimming with setOpacity()

We also included a small utility to fine-tune the text color’s visibility — for subtle use cases like secondary labels or disabled states:

export const setOpacity = (hex: string, alpha: number) =>
    `${hex}${Math.floor(alpha * 255)
        .toString(16)
        .padStart(2, "0")}`;
Enter fullscreen mode Exit fullscreen mode

This function is used in the dimRate prop of the component to apply alpha transparency directly to the color.

🧠 What it does:
It takes:

  • A base hex color (e.g. #000000)
  • An alpha value from 0 to 1
  • And appends a two-digit hex representation of that alpha

🧪 Example:

setOpacity("#000000", 0.5); // → "#00000080" (50% transparent black)
setOpacity("#FF5733", 0.2); // → "#FF573333" (20% transparent orange)
Enter fullscreen mode Exit fullscreen mode

This makes it super easy to implement things like:

<Text dimRate="60%">This text is slightly dimmed</Text>
Enter fullscreen mode Exit fullscreen mode

🚀 Wrapping Up

Responsive typography isn’t a “nice-to-have”—it’s essential. With a little setup:

  • Your text becomes readable across all devices,
  • Your layouts break less,
  • And your app just feels more polished.

📦 Bundle your responsiveFontSize() and <Text /> component, and you’ve got a future-proof solution that your whole team can use.


🧠 TL;DR

Feature Static Font Responsive Font
Scales to screen size?
Looks consistent across devices?
Respects design system? 🧐
Makes users happy? 😐 😍

💬 Got questions?

Got questions about scaling fonts, making text more accessible, or handling dark mode like a pro? Let me know in the comments — happy to help you level up your UI game!


📎 Resources

Top comments (1)

Collapse
 
androaddict profile image
androaddict

Nice one