skip to content

Custom Fonts & Typography

Custom fonts are bundled and linked into native projects or loaded with expo-font, then named by fontFamily, which differs per platform. Interviewers probe bold text falling back to the system font.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a bare React Native project, what must you do so a bundled .ttf file can be used through the Text fontFamily style?

level: juniorimportance: must knowfreq 55%

answer

  1. fonts are native resources, not JS
  2. Android: assets/fonts, file name is the family
  3. iOS: target resource plus UIAppFonts
  4. rebuild, a Metro reload is not enough
  5. unknown name falls back silently

basics

~10 s

Copy the font into each native project: android/app/src/main/assets/fonts on Android, and the iOS app target plus a UIAppFonts entry in Info.plist. Then rebuild the native app and reference it by name in fontFamily.

solid answer

~40 s

A custom font is a native resource, so a Metro reload never picks it up. On Android I copy the `.ttf` or `.otf` into `android/app/src/main/assets/fonts/`; React Native's font manager looks there by file name, so `BrandSans-Regular.ttf` is used as `fontFamily: 'BrandSans-Regular'`. On iOS I add the file to the app target so it is copied into the bundle, and list it under `UIAppFonts` in `Info.plist`; iOS then knows it by the family or PostScript name stored inside the file. Then I rebuild both apps. If a name is wrong nothing throws: the text silently renders in the system font. In an Expo project the `expo-font` config plugin makes the same native edits at prebuild.

code

xml · 7 lines
xml
<key>UIAppFonts</key>
<array>
  <string>BrandSans-Regular.ttf</string>
  <string>BrandSans-Medium.ttf</string>
  <string>BrandSans-SemiBold.ttf</string>
  <string>BrandSans-Bold.ttf</string>
</array>

go deeper

for a junior

Recall the two native steps: Android assets/fonts, and on iOS a target resource plus a UIAppFonts entry. Then say you rebuild the app, because fonts are not JavaScript.

for a middle

Explain why the names differ: Android's asset lookup uses the file name, iOS uses names stored in the font. Renaming files to their PostScript names makes one string work everywhere.

for a senior

Show how you keep this from regressing: automate the linking, verify the built Info.plist and assets folder, and treat a silent system-font fallback as a test failure, not a cosmetic nit.

for a principal

Frame fonts as native build inputs: decide whether the team links them by hand, through a config plugin, or through a tool, and who owns the check that every platform build actually contains them.

## Why a custom font is a native resource In React Native, a `<Text>` is drawn by a native text view, and the typeface it uses is looked up by the platform's font system, not by JavaScript. The `fontFamily` style is only a string that React Native passes to that lookup. The font file therefore has to be inside the installed app, in the Android APK's assets or the iOS app bundle, before the string can resolve to anything. Two consequences are worth saying out loud in an interview: - **A Metro reload is not enough.** Fast Refresh swaps JavaScript; it never repackages native resources. Adding or renaming a font file means rebuilding and reinstalling the native app. - **A missing font fails silently.** React Native does not throw when a family is unknown. iOS draws with the system font at the requested size and weight; Android tries its asset lookup and then falls back to a system typeface. The screen simply looks slightly wrong, which is why typos survive code review. ## Android: the assets/fonts folder React Native's Android font manager, `ReactFontManager`, resolves a `fontFamily` string in this order: 1. A family registered from native code with `ReactFontManager.getInstance().addCustomFont(...)`, typically an XML font family under `res/font`. 2. A file in the APK's `assets/fonts/` folder whose base name equals the string, trying `.ttf` and then `.otf`. 3. Otherwise, a system typeface created from the name. So the bare-project step is to copy `BrandSans-Regular.ttf` into `android/app/src/main/assets/fonts/`. The **file name is the family name**: `fontFamily: 'BrandSans-Regular'`. The asset lookup also understands the suffixes `_bold`, `_italic` and `_bold_italic` for the four classic variants of one family, which matters as soon as you combine a custom family with `fontWeight`. ## iOS: bundle resource plus UIAppFonts iOS needs two separate things, and forgetting either one gives the same silent fallback: 1. The font file must be a **member of the app target**, so Xcode copies it into the bundle (it shows up in the target's Copy Bundle Resources phase). 2. The file name must be listed in `Info.plist` under the **`UIAppFonts`** key, which Xcode displays as "Fonts provided by application". iOS registers the listed files when the app launches. After that, iOS knows the font by the names stored **inside** the file: its family name (for example `BrandSans`) and each face's PostScript name (for example `BrandSans-Regular`). React Native accepts either. It first treats the string as a family; if no family matches, it tries it as a font name and recovers the family from it. ## Naming it in fontFamily | | Android (assets/fonts) | iOS (bundle + UIAppFonts) | |---|---|---| | Where the name comes from | the file name, without extension | metadata inside the font file | | Example string | `'BrandSans-Regular'` for `BrandSans-Regular.ttf` | `'BrandSans'` or `'BrandSans-Regular'` | | Unknown name | a system typeface | the system font | The practical rule is to **rename each file to its PostScript name** before copying it; then one `fontFamily` string works on both platforms. Note also that `fontFamily` takes exactly one name. Unlike CSS `font-family`, there is no comma-separated fallback list, so `'BrandSans, sans-serif'` is simply an unknown family. ## Formats worth choosing Ship static **TTF or OTF** files. React Native's Android asset lookup only tries those two extensions, and variable fonts do not have consistent support across platforms, so the usual advice is one static file per weight and style. If the design team hands over a variable font, extract the specific instances you need into separate files first. ## Automation, Expo and verification Hand-editing two native projects is error-prone, so teams usually automate it. In an Expo project the `expo-font` config plugin performs the same edits during prebuild: on iOS it adds the files to the target and to `UIAppFonts`; on Android it copies `.ttf` and `.otf` files into `assets/fonts`, or into `res/font` with a generated XML family when you describe weights. Community asset-linking tools do the copying for bare apps. Whichever route you take, check the result rather than trusting it: - open the built app's `Info.plist` and confirm every `UIAppFonts` entry matches a real file name; - confirm the files sit in `android/app/src/main/assets/fonts` in the Android project; - render a test string next to one in the system font on both platforms; a silent fallback is the usual symptom of a typo or a missed step; - rebuild after every font change, and do not debug a missing font with a JavaScript reload.

  • Why does a newly added font not appear after reloading Metro or saving a file with Fast Refresh on?
    Fast Refresh and a Metro reload replace only the JavaScript bundle. React Native's native font lookup reads the APK's `assets/fonts` and the iOS bundle's registered `UIAppFonts`, which are packaged when the native app is built. A new or renamed font file therefore needs a native rebuild and reinstall before `fontFamily` can resolve it.
  • What does React Native do at runtime when fontFamily names a font the app does not contain?
    Nothing visible to JavaScript: there is no red box or thrown error. On iOS React Native uses the system font at the requested size and weight. On Android the font manager tries `fonts/<name>.ttf` and `.otf` in the assets, then falls back to a system typeface. The text renders, just in the wrong face.

saying these in an interview costs you the question

  • Dropping the font into a JavaScript assets folder and reloading Metro is enough.
  • fontFamily accepts a CSS-style fallback list such as 'BrandSans, sans-serif'.
  • A misspelled fontFamily throws a red-box error so you will notice it.
  • On iOS the file only needs to be in the project folder, no Info.plist entry.
  • On Android the file name does not matter because the family is read from inside the file.
open as a page

In React Native, why can one fontFamily string find a custom font on iOS but not on Android, and how do you fix the naming?

level: middleimportance: must knowfreq 48%

basics

~20 s

iOS resolves fontFamily against names stored inside the font, its family or PostScript name, while Android's asset lookup matches the file name. Renaming each file to its PostScript name, or a per-platform mapping, makes one string work.

open as a page

In an Expo project, when would you embed a custom font with the expo-font config plugin instead of loading it with useFonts?

level: middleimportance: should knowfreq 42%

basics

~20 s

Embed with the expo-font config plugin for native builds: the font ships in the binary and is usable on the first frame. Use useFonts for Expo Go or web, accepting an async load and a render gate.

open as a page

In a React Native fashion store app bundling a brand typeface in four weights, why do headings styled fontWeight '700' render in the system font on Android but correctly on iOS?

level: seniorimportance: should knowfreq 38%

basics

~20 s

iOS picks the nearest weight among the family's registered faces. Android's asset lookup only knows regular, _bold, _italic and _bold_italic files named after fontFamily, so fontWeight '700' on 'BrandSans-Regular' finds no file and falls back to the system font.

open as a page

In React Native, why can text in a custom font with the same fontSize and lineHeight sit at different heights on Android and iOS?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Each platform builds the line box from the font's own ascent and descent differently. Android adds font padding by default (includeFontPadding: true) and splits lineHeight's extra space above and below; iOS pins the line height and shifts the baseline.

open as a page