Custom icon fonts with React Native

When working with icons in React Native apps we're spoilt for choice with a wide range of free and open-source icon sets such as FontAwesome, Material and Ionicons. To make things even easier, the wonderful react-native-vector-icons project bundles all of those icon sets plus more into one single package. But sometimes free and open-source icon sets just don't cut it and you're left wondering how to achieve something that has the same developer experience for a custom icon set. Fortunately, react-native-vector-icons and a bunch of other projects have us covered here too.

Setting up react-native-vector-icons

If you're using Expo and have not ejected to ExpoKit then there's nothing to do here. Expo bundles a wrapper around react-native-vector-icons in the @expo/icons package.

Otherwise, the installation of the react-native-vector-icons package is as you would expect for a React Native app. It's published to npm so you can add it to your project with the CLI or equivalent (we tend to use Yarn when working with React Native because it plays better with Expo):

$ yarn add react-native-vector-icons
$ react-native link react-native-vector-icons

Generating an icon font

With react-native-vector-icons set up in your project you are ready to work on the icons themselves. In our experience IcoMoon is the most effective tool here. IcoMoon is a web application that allows you to import SVG files and produce font files in various formats from collections of those SVGs, as shown in the following screenshot:

Creating an icon set in IcoMoon An example of creating an icon set in IcoMoon

Once all of your icons are imported to the IcoMoon app you can select them and "Generate" the font file (note that in the screenshot below it shows the number of selected icons to the left of the highlighted "Generate Font" button):

Generating an icon font in IcoMoon An example of generating an icon font from an icon set in IcoMoon

There are a few options to configure the resulting font but most of the time the defaults will suffice. When you're happy download the bundle and unzip it to find a selection of font files, some examples and a selection.json file. It's that file plus the *.ttf font file that we need. Copy those files to a sensible directory within your React Native codebase. We usually go for a top- level assets directory which contains all of the static assets used by the app including fonts and images.

Using the custom icon font

It's recommended that you pre-load any fonts that your app is going to use and your new custom icon font is no exception. In your main app entry point you can use the Font.loadAsync method. If you have used the Expo CLI to initialise your project then you probably have something that looks like this already:

import React from 'react';
import { registerRootComponent, AppLoading } from 'expo';
import * as Font from 'expo-font';

class App extends React.Component {
  state = {
    isLoadingComplete: false,
  };

  loadResourcesAsync = async () => Promise.all([
    Font.loadAsync({
      'custom-icons': require('../assets/fonts/custom-icons.ttf'),
    }),
  ]);

  handleLoadingError = (error) => {
    // In this case, you might want to report the error to your error
    // reporting service, for example Sentry
    console.warn(error);
  };

  handleFinishLoading = () => {
    this.setState({ isLoadingComplete: true });
  };

  render() {
    const { isLoadingComplete } = this.state;

    if (!isLoadingComplete) {
      return (
        <AppLoading
          startAsync={this.loadResourcesAsync}
          onError={this.handleLoadingError}
          onFinish={this.handleFinishLoading}
        />
      );
    }

    return (
      <App />
    );
  }
}

registerRootComponent(App);

// Export the App component for unit testing purposes. Expo handles rendering
// via the "registerRootComponent" call above and does not require an export.
export default App;

With this configuration your custom icon font file will be loaded at app start- up rather than at first usage which would otherwise result in flashes of unstyled (or missing) content.

Next up you need a normal React component to render icons from your new font. The react-native-vector-icons package provides some utility methods to make this process simpler. The following few lines are all that are needed. We usually place this in a src/components/icon/index.js file:

import { createIconSetFromIcoMoon } from 'react-native-vector-icons';
import icoMoonConfig from '../../../assets/fonts/selection.json';

// We use the IcoMoon app (https://icomoon.io) to generate a custom font made up
// of SVG icons. The actual font file is loaded up-front in src/index.js.
export default createIconSetFromIcoMoon(icoMoonConfig, 'custom-icons');

The key points to note here are the import of the selection.json file from the bundle downloaded from IcoMoon and the name of the font, custom-icons, as defined in the Font.loadAsync call in the main app entry point.

The createIconSetFromIcoMoon function could be thought of as a factory that returns a React component. You can now import that component from your other components to render icons. The following example imagines a simple "button" component in src/components/button/index.js:

import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
import Icon from '../icons';

const Button = () => (
  <TouchableOpacity>
    <Icon name="settings" />
    <Text>Settings</Text>
  </TouchableOpacity>
);

export default Button;

The new Icon component supports all of the props that the open-source icon sets bundled with react-native-vector-icons support. This means you can apply custom styles, such as sizes and colours, from React Native stylesheets.

Twitter