DEV Community

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

Posted on

3

🔠 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

Gen AI apps are built with MongoDB Atlas

Gen AI apps are built with MongoDB Atlas

MongoDB Atlas is the developer-friendly database for building, scaling, and running gen AI & LLM apps—no separate vector DB needed. Enjoy native vector search, 115+ regions, and flexible document modeling. Build AI faster, all in one place.

Start Free

Top comments (1)

Collapse
 
androaddict profile image
androaddict

Nice one

Not Sure What Type of Protection is Best for Your Mobile App?

Not Sure What Type of Protection is Best for Your Mobile App?

While mobile app wrappers can be easy to implement security measures, they also present a single point of failure in an attack. Compilers integrate multiple security checks, offering a stronger and more robust security solution than wrappers.

Learn more

👋 Kindness is contagious

Explore this practical breakdown on DEV’s open platform, where developers from every background come together to push boundaries. No matter your experience, your viewpoint enriches the conversation.

Dropping a simple “thank you” or question in the comments goes a long way in supporting authors—your feedback helps ideas evolve.

At DEV, shared discovery drives progress and builds lasting bonds. If this post resonated, a quick nod of appreciation can make all the difference.

Okay