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:
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):
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.