Skip to main content

Custom Vector Icons

Overview

As trendy as it is these days, not every app has to use emojis for all icons. Perhaps you want to incorporate a popular set through an icon font, such as FontAwesome, Glyphicons, or Ionicons, or maybe even use your own custom icons.

In this example, we will be implementing FontAwesome 6. This tutorial is written for the Ignite v9 CNG workflow; however, it generally still applies to a DIY or even a bare React Native project

Installation

If you haven't already, spin up a new Ignite application:

npx ignite-cli@next new PizzaApp --remove-demo --workflow=cng --yes
cd PizzaApp

Next, let's install the necessary dependencies. You can see complete installation instructions for @expo/vector-icons here.

npx expo install @expo/vector-icons
info

The goal of this recipe is to utilize custom icon fonts such as FontAwesome 6, which you will need to download from elsewhere.

For built-in icon fonts from @expo/vector-icons, you can skip the following setup and proceed directly to modifying the Icon component section.

Font Assets

Once everything is installed, it's now time to download the actual fonts that we're going to use to render our icons. First, download your font and place all .ttf files in our assets/fonts folder.

ignite-project
├── app
├── ...
├── assets
│ ├── icons
│ ├── images
│ └── fonts
│ ├── fa-light-300.ttf
│ ├── fa-regular-400.ttf
│ ├── fa-solid-900.ttf
│ ├── fa-thin-100.ttf
│ └── fa-brands-400.ttf
├── ...
└── package.json

Import our fonts

It's now time to implement these icons to our Icon.tsx component. We will be modifying the iconRegistry object to map our icon names and all other changes explained below.

First, open app/components/Icon.tsx and then import createMultiStyleIconSet from @expo/vector-icons.

 import {
- Image,
ImageStyle,
StyleProp,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
+ TextProps,
+ Platform,
} from "react-native"
+import { createMultiStyleIconSet } from '@expo/vector-icons'

Next, we will re-define our iconRegistry and create our own custom Icon component. We have our handy function to do it below.

const iconFonts = {
thin: require('../../assets/fonts/fa-thin-100.ttf'),
light: require('../../assets/fonts/fa-light-300.ttf'),
regular: require('../../assets/fonts/fa-regular-400.ttf'),
solid: require('../../assets/fonts/fa-solid-900.ttf'),
brand: require('../../assets/fonts/fa-brands-400.ttf'),
}

/**
* We are not using icon names
* Why?
* - Reduce bundle size
* - Flexible & consistent names
* - Performance(?)
*
* How to add icons?
* 1. Goto https://fontawesome.com/search
* 2. Search for the icon you need
* 3. Open the icon and copy the Unicode value
* 4. Finally, map it below with a friendly name
*/
export const iconRegistry = {
back: 'f060',
bell: 'f0f3',
caretLeft: 'f0d9',
caretRight: 'f0da',
check: 'f00c',
clap: 'e1a8',
community: 'f500',
components: 'f5fd',
debug: 'f120',
github: 'f09b',
heart: 'f004',
hidden: 'f070',
ladybug: 'f188',
lock: 'f023',
menu: 'f0c9',
more: 'f141',
pin: 'f3c5',
podcast: 'f2ce',
settings: 'f013',
slack: 'f198',
view: 'f06e',
x: 'f00d',
}

const createFontAwesomeStyle = (style: IconStyle, fontWeight: string) => {
const fontFile = iconFonts[style]
return {
fontFamily: `Font Awesome 6 Pro ${style}`,
fontFile,
fontStyle: Platform.select({
ios: {
fontWeight,
},
default: {},
}),
glyphMap: Object.entries(iconRegistry).reduce<{ [key: string]: number }>((acc, [name, unicode]) => {
acc[name] = parseInt(unicode, 16)
return acc
}, {}),
}
}

VectorIcon Component

Now, it's time to create our custom VectorIcon component. Take note of the available styles for our icon. These are specific to FontAwesome, and we're defining the theme here.

export type IconStyle = keyof typeof iconFonts
interface VectorIconProps extends TextProps, Partial<Record<IconStyle, boolean>> {
name?: IconTypes
size?: number
color?: string
width?: string | number
height?: string | number
}

export const VectorIcon: ComponentType<VectorIconProps> & {
font: { [x: string]: string }
} = createMultiStyleIconSet(
{
thin: createFontAwesomeStyle('thin', '100'),
light: createFontAwesomeStyle('light', '300'),
regular: createFontAwesomeStyle('regular', '400'),
solid: createFontAwesomeStyle('solid', '900'),
brand: createFontAwesomeStyle('brand', '400'),
},
// Default font style
{ defaultStyle: 'regular' },
)

Preloading our Fonts

Let's modify our app/app.tsx to pre-load our fonts during hyrdration. You can learn more here

+import { VectorIcon } from "./components"

- const [areFontsLoaded] = useFonts(customFontsToLoad)
+ const [areFontsLoaded] = useFonts({
+ ...customFontsToLoad,
+ ...VectorIcon.font,
+ })

Modify the Icon Component

Now that we have our VectorIcon, it's time to use it within our Icon component! Let's modify our IconProps to include the styles extension, making it easier to set when using the component.

note

If you only need to use a built-in icon from @expo/vector-icons, simply replace VectorIcon with the specific icon you need.

import VectorIcon from "@expo/vector-icons/Ionicons"

Make a few adjustments to the props here and there, and you'll be all set!

-interface IconProps extends TouchableOpacityProps
+interface IconProps extends TouchableOpacityProps, Partial<Record<IconStyle, boolean>>
const {
icon,
color,
size,
- style: $imageStyleOverride,
+ style: $iconStyleOverride,
containerStyle: $containerStyleOverride,
+ thin,
+ light,
+ regular,
+ solid,
+ brand,
...WrapperProps
} = props
  return (
<Wrapper
accessibilityRole={isPressable ? "imagebutton" : undefined}
{...WrapperProps}
style={$containerStyleOverride}
>
- <Image
- style={[
- $imageStyle,
- color && { tintColor: color },
- size && { width: size, height: size },
- $imageStyleOverride,
- ]}
- source={iconRegistry[icon]}
+ <VectorIcon
+ name={icon}
+ size={size}
+ color={color}
+ style={$iconStyleOverride}
+ thin={thin}
+ light={light}
+ regular={regular}
+ solid={solid}
+ brand={brand}
/>
</Wrapper>
)

Conclusion

Here's the modified app/components/Icon.tsx.

import * as React from "react"
import { ComponentType } from "react"
import {
ImageStyle,
StyleProp,
TextProps,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
Platform,
} from "react-native"
import { createMultiStyleIconSet } from '@expo/vector-icons'

export type IconStyle = keyof typeof iconFonts
export type IconTypes = keyof typeof iconRegistry

// Downloaded from our FA pro-ness pack
const iconFonts = {
thin: require('../../assets/fonts/fa-thin-100.ttf'),
light: require('../../assets/fonts/fa-light-300.ttf'),
regular: require('../../assets/fonts/fa-regular-400.ttf'),
solid: require('../../assets/fonts/fa-solid-900.ttf'),
brand: require('../../assets/fonts/fa-brands-400.ttf'),
}

const createFontAwesomeStyle = (style: IconStyle, fontWeight: string) => {
const fontFile = iconFonts[style]
return {
fontFamily: `Font Awesome 6 Pro ${style}`,
fontFile,
fontStyle: Platform.select({
ios: {
fontWeight,
},
default: {},
}),
glyphMap: Object.entries(iconRegistry).reduce<{ [key: string]: number }>((acc, [name, unicode]) => {
acc[name] = parseInt(unicode, 16)
return acc
}, {}),
}
}

interface IconProps extends TouchableOpacityProps, Partial<Record<IconStyle, boolean>> {
/**
* The name of the icon
*/
icon: IconTypes

/**
* An optional tint color for the icon
*/
color?: string

/**
* An optional size for the icon. If not provided, the icon will be sized to the icon's resolution.
*/
size?: number

/**
* Style overrides for the icon image
*/
style?: StyleProp<ImageStyle>

/**
* Style overrides for the icon container
*/
containerStyle?: StyleProp<ViewStyle>

/**
* An optional function to be called when the icon is pressed
*/
onPress?: TouchableOpacityProps["onPress"]
}

interface VectorIconProps extends TextProps, Partial<Record<IconStyle, boolean>> {
name?: IconTypes
size?: number
color?: string
width?: string | number
height?: string | number
}

/**
* A component to render a registered icon.
* It is wrapped in a <TouchableOpacity /> if `onPress` is provided, otherwise a <View />.
*
* - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Icon.md)
*/
export function Icon(props: IconProps) {
const {
icon,
color,
size,
style: $iconStyleOverride,
containerStyle: $containerStyleOverride,
thin,
light,
regular,
solid,
brand,
...WrapperProps
} = props

const isPressable = !!WrapperProps.onPress
const Wrapper: ComponentType<TouchableOpacityProps> = WrapperProps?.onPress
? TouchableOpacity
: View

return (
<Wrapper
accessibilityRole={isPressable ? "imagebutton" : undefined}
{...WrapperProps}
style={$containerStyleOverride}
>
<VectorIcon
name={icon}
size={size}
color={color}
style={$iconStyleOverride}
thin={thin}
light={light}
regular={regular}
solid={solid}
brand={brand}
/>
</Wrapper>
)
}

export const iconRegistry = {
back: 'f060',
bell: 'f0f3',
caretLeft: 'f0d9',
caretRight: 'f0da',
check: 'f00c',
clap: 'e1a8',
community: 'f500',
components: 'f5fd',
debug: 'f120',
github: 'f09b',
heart: 'f004',
hidden: 'f070',
ladybug: 'f188',
lock: 'f023',
menu: 'f0c9',
more: 'f141',
pin: 'f3c5',
podcast: 'f2ce',
settings: 'f013',
slack: 'f198',
view: 'f06e',
x: 'f00d',
}

export const VectorIcon: ComponentType<VectorIconProps> & {
font: { [x: string]: string }
} = createMultiStyleIconSet(
{
thin: createFontAwesomeStyle('thin', '100'),
light: createFontAwesomeStyle('light', '300'),
regular: createFontAwesomeStyle('regular', '400'),
solid: createFontAwesomeStyle('solid', '900'),
brand: createFontAwesomeStyle('brand', '400'),
},
// Default font style
{ defaultStyle: 'regular' },
)

That's all there is to it! We only added the optional styles prop so if you're using Ignite, things should work.

<Icon solid icon="community" color={colors.tint} size={24} />
<Icon light icon="check" color={colors.tint} size={24} />

Pro tip

It is recommend to put the config under app/themes/icons.ts to keep things organized.

Is this page still up to date? Did it work for you?