Normalizing Text and Spacing Between Screen Sizes

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: March 27, 2019

Example.js

const styles = StyleSheet.create({
  text: {
    color: '#d4895e',
    fontWeight: 'bold',
    fontSize: 40,
    marginHorizontal: 20,
  },
});

That's pretty standard to have in your styles, right? I'd say so. It's what most of my code looks like!

But how does that look on different devices?

My phone is an iPhone X - it's also the device I use as a simulator when developing my apps. It's a phone with a big beautiful screen and I base everything on it.

But what happens if I spin up that same app on an iPhone 5? Or an iPhone 4? What about an older android device? Or lower budget one?

Comparison between devices with same styles.

There's a lot of options out there and we need to factor for those in our designs. A 40 pixel font size is going to look drastically different on an iPhone XS Max vs. an iPhone 4.

Fortunately, accounting for this doesn't have to be hard.

There are two factors we'll want to take into consideration when "normalizing" font sizes, padding, margin, etc. between different devices.

  1. Pixel Density/Ratio
  2. Screen Dimensions

PixelRatio in React Native

PixelRatio docs

PixelRatio has a variety of functions. In this context we're interested in PixelRatio.get(), which returns the pixel density of the device.

This aligns with the @1x, @2x, and @3x you see on iOS or mdpi, hdpi, xhdpi, etc. you see on Android.

We'll use this as a first pass to determine at what ratio we want to modify the sizes.

Check out the other functions available to you from PixelRatio as well and let me know if you want to cover them in more detail.

Dimensions in React Native

Dimensions docs

Dimensions is pretty straightforward. We're going to use it to get the width and height of the device window.

Why? Well this will give us a good reference on how much to scale the number we're working with. If it's a narrow device (small width) we may want to scale the font size down so each line doesn't only have one word.

React Native Elements: A Fantastic Resource

Before we dive into code I wanted to give credit where credit is due: React Native Elements. This UI toolkit is not only great to use but it's also a fantastic reference on good patterns in React Native.

I've used their normalizeText file as a reference for numerous projects, including today's lesson. I would highly encourage you to check it out.

The Normalize Function

I'll typically put a normalize function in some sort of utility file, like src/utilities/normalize.js and import wherever I need it in my app.

In this file I'll typically grab the PixelRatio once and the device dimensions each time the function is called.

src/utilities/normalize.js

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

const ratio = PixelRatio.get();

const normalize = (size) => {
  const { width, height } = Dimensions.get('window');

  return size;
};

export default normalize;

Why grab the width/height each time the function is called? Because it can change depending on device orientation. I've also found that the overhead of doing this is minimal/non-existent.

Categorizing By PixelRatio

Next, we'll go ahead and break the code into three general categories based on the device pixel ratio.

src/utilities/normalize.js

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

const ratio = PixelRatio.get();

const normalize = (size) => {
  const { width, height } = Dimensions.get('window');

  if (ratio >= 2 && ratio < 3) {
    // scale
  } else if (ratio >= 3 && ratio < 3.5) {
    // scale
  } else if (ratio >= 3.5) {
    // scale
  }

  return size;
};

export default normalize;

With this we're using a pixel ratio of 1 as our baseline and we'll then scale fonts based on that, breaking it into common categories. Typically a ration is going to be 1, 2, or 3 but they can fall in between them as well (looking at you Android) so we'll work with ranges.

Categorizing By Dimensions

This is the first spot your discretion is going to come into play - where are the "breakpoints" within your app that you should start modify font sizes? I'm going to follow what React Native Elements uses because it's always worked for me in the past.

They're the same break points for each pixel density.

src/utilities/normalize.js

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

const ratio = PixelRatio.get();

const normalize = (size) => {
  const { width, height } = Dimensions.get('window');

  if (ratio >= 2 && ratio < 3) {
    if (width < 360) {
      // scale
    } else if (height < 667) {
      // scale
    } else if (height >= 667 && height <= 735) {
      // scale
    }

    // scale
  } else if (ratio >= 3 && ratio < 3.5) {
    if (width < 360) {
      // scale
    } else if (height < 667) {
      // scale
    } else if (height >= 667 && height <= 735) {
      // scale
    }

    // scale
  } else if (ratio >= 3.5) {
    if (width < 360) {
      // scale
    } else if (height < 667) {
      // scale
    } else if (height >= 667 && height <= 735) {
      // scale
    }

    // scale
  }

  return size;
};

export default normalize;

We've got three categories:

  1. Device width is less than 360 (narrow phone)
  2. Device height less than 667
  3. Device height between 667 and 735
  4. The rest.

The Scaling Factor

This is really where your discretion is going to come into play. You'll have to tinker with the scaling ratio to get it to look just right. Typically the scaling ratio is going to be +/- 0.2 (unless you're targeting tablets as well).

I'll show you the final value in the "Final Code" section, again based on React Native Element's values. Use it as a reference point and modify based on your needs, just like you would device breakpoints.

Simplifying your Code

If you want to do your normalizing in bulk you can just create StyleSheet.create with the following code. It will look for a series of target keys and normalize those automatically otherwise it will just forward them along without any changes.

src/utilities/normalize.js

// ...

export const create = (
  styles,
  targetProperties = [
    'fontSize',
    'margin',
    'marginHorizontal',
    'marginVertical',
    'padding',
    'paddingVertical',
    'paddingHorizontal',
    'height',
  ]
) => {
  const normalizedStyles = {};
  Object.keys(styles).forEach((key) => {
    normalizedStyles[key] = {};
    Object.keys(styles[key]).forEach((property) => {
      if (targetProperties.includes(property)) {
        normalizedStyles[key][property] = normalize(styles[key][property]);
      } else {
        normalizedStyles[key][property] = styles[key][property];
      }
    });
  });

  return StyleSheet.create(normalizedStyles);
};

src/Example.js

import { create } from '../utilities/normalize.js';

const styles = create({
  text: {
    color: '#d4895e',
    fontWeight: 'bold',
    fontSize: 40,
    marginHorizontal: 20,
  },
});

Comparison between devices with same styles but normalized.

Final Code

src/utilities/normalize.js

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

const ratio = PixelRatio.get();

const normalize = (size) => {
  const { width, height } = Dimensions.get('window');

  if (ratio >= 2 && ratio < 3) {
    if (width < 360) {
      return size * 0.95;
    } else if (height < 667) {
      return size;
    } else if (height >= 667 && height <= 735) {
      return size * 1.15;
    }

    return size * 1.25;
  } else if (ratio >= 3 && ratio < 3.5) {
    if (width < 360) {
      return size;
    } else if (height < 667) {
      return size * 1.15;
    } else if (height >= 667 && height <= 735) {
      return size * 1.2;
    }

    return size * 1.27;
  } else if (ratio >= 3.5) {
    if (width < 360) {
      return size;
    } else if (height < 667) {
      return size * 1.2;
    } else if (height >= 667 && height <= 735) {
      return size * 1.25;
    }

    return size * 1.4;
  }

  return size;
};

export const create = (
  styles,
  targetProperties = [
    'fontSize',
    'margin',
    'marginHorizontal',
    'marginVertical',
    'padding',
    'paddingVertical',
    'paddingHorizontal',
    'height',
  ]
) => {
  const normalizedStyles = {};
  Object.keys(styles).forEach((key) => {
    normalizedStyles[key] = {};
    Object.keys(styles[key]).forEach((property) => {
      if (targetProperties.includes(property)) {
        normalizedStyles[key][property] = normalize(styles[key][property]);
      } else {
        normalizedStyles[key][property] = styles[key][property];
      }
    });
  });

  return StyleSheet.create(normalizedStyles);
};

export default normalize;
React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.

Join the email list to be notified of all new lessons and classes!