skip to content

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%

answer

  1. iOS reads names inside the file
  2. family name or PostScript name
  3. Android asset lookup uses the file name
  4. rename files to the PostScript name
  5. one name only, no fallback list

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.

solid answer

~40 s

On iOS, React Native treats the string first as a font **family** and, if none matches, as a font's **PostScript name**, both read from the font file's metadata. On Android, a font copied into `assets/fonts` is found by its **file name** without extension; a font registered as an XML family is found by the name it was registered under. So a file called `brandsans_semibold.ttf` whose PostScript name is `BrandSans-SemiBold` needs two different strings. The fix is to rename each file to its PostScript name so one string works, or to map names with `Platform.select` inside a single typography module. `fontFamily` also takes exactly one name, so there is no CSS-style fallback list to paper over the mismatch.

code

typescript · 10 lines
typescript
import { Platform } from 'react-native';

// Package files keep their own names, so map them once per platform
export const fonts = {
  heavy: Platform.select({
    ios: 'Inter-Black', // PostScript name read from the file
    android: 'Inter_900Black', // file name in assets/fonts
    default: 'Inter_900Black',
  }),
};

go deeper

for a junior

Recall that fontFamily is one string and that each platform looks it up differently, so a font can work on one device and silently fall back on the other.

for a middle

Explain both lookups: iOS uses the family or PostScript name inside the file, Android's asset lookup uses the file name. Renaming files to PostScript names is the usual fix.

for a senior

Show how you stop the regression: one typography module owns every family name, Android families are registered deliberately, and a visual check on both platforms runs in CI.

for a principal

Treat font naming as a contract between design, build tooling and code; choose one naming convention for every shipped face and make the build, not reviewers, enforce it.

## One string, two lookup rules React Native's `fontFamily` style is a single string, and each platform's text renderer resolves it differently. The mismatch is one of the most common "works on iOS, broken on Android" font bugs, and it is invisible until someone looks at an Android device, because an unresolved name falls back silently to a system typeface. ## How iOS resolves the string A font file carries metadata: a **family name** shared by all its faces (for example `BrandSans`) and a unique **PostScript name** per face (for example `BrandSans-SemiBold`). The PostScript name is not the display name you see in a design tool. When iOS registers a bundled font listed in `UIAppFonts`, those embedded names are what it knows. React Native's iOS font code: 1. looks the string up as a **family**, and if it finds one, picks the face closest to the requested `fontWeight` and style; 2. if there is no such family, tries the string as a **font name**, such as a PostScript name, and then works out that font's family; 3. if neither matches, falls back to the system font without any error. The **file name plays no part** on iOS. Renaming `BrandSans-SemiBold.otf` to `x.otf` changes nothing about how it is addressed. ## How Android resolves the string On Android, React Native's `ReactFontManager` has no access to that metadata for plain asset fonts. It resolves: 1. a family registered from native code with `addCustomFont`, typically an XML font family in `res/font`, by the name it was registered under; 2. otherwise a file in `assets/fonts` whose **file name**, minus `.ttf` or `.otf`, equals the string; 3. otherwise a system typeface. Here the **metadata plays no part**: `fontFamily: 'BrandSans-SemiBold'` only finds `assets/fonts/BrandSans-SemiBold.ttf`. ## The mismatch in practice | File on disk | PostScript name | iOS string | Android string | |---|---|---|---| | `brandsans_semibold.ttf` | `BrandSans-SemiBold` | `'BrandSans-SemiBold'` or `'BrandSans'` | `'brandsans_semibold'` | | `BrandSans-SemiBold.ttf` | `BrandSans-SemiBold` | `'BrandSans-SemiBold'` | `'BrandSans-SemiBold'` | Expo's own documentation shows the same effect with a Google Fonts package embedded through the `expo-font` config plugin: the file `Inter_900Black.ttf` is addressed as `'Inter_900Black'` on Android but as `'Inter-Black'` on iOS. ## Making one name work 1. **Rename files to their PostScript names** before linking them. This is Expo's recommendation for fonts embedded as a plain path list, and it removes the problem at the source. You can read the PostScript name in a font inspector such as the macOS Font Book app. 2. **Register a named family on Android** with the `expo-font` plugin's object syntax (`fontFamily` plus `fontDefinitions`), or in a bare app with an XML font family passed to `ReactFontManager.getInstance().addCustomFont`. Android then answers to the family name you chose, and you can make it match the iOS family name. 3. **Map per platform in one place** with `Platform.select` when you cannot rename, for example with third-party font packages. Keep that map in a typography module so no screen hard-codes platform names. ## Where the expo-font config plugin fits The `expo-font` config plugin does not remove the two lookup rules; it only automates the linking. Given a plain list of paths, it copies each file into Android's `assets/fonts`, where it is addressed by file name, while iOS still reads the names inside the file. That is why Expo's guide tells you to name each file after its PostScript name when you use the path list. With the object syntax under `android.fonts`, you choose the Android family name yourself in `fontFamily` and the plugin registers an XML family under it; iOS, in the same config, always takes the family from the file. Choosing the same string for both, the family name embedded in the files, gives one portable `fontFamily` and working `fontWeight` selection on both platforms. ## Related rules worth knowing - `fontFamily` accepts **one** name. `'BrandSans, Helvetica'` is not a stack; it is an unknown family and falls back to the system font. - The iOS docs for React Native 0.87 list generic families (`system-ui`, `ui-sans-serif`, `ui-serif`, `ui-monospace`, `ui-rounded`) as supported on iOS; they are not portable to Android. - Because nothing throws, protect the names with a check: a screenshot or visual test on both platforms, or a small script that compares the typography module's names with the files you ship.

  • On iOS, does it matter whether you pass a React Native fontFamily the family name or the PostScript name?
    Both resolve. React Native first tries the string as a family and chooses the face nearest the requested `fontWeight` and style; if no family matches, it treats the string as a font name such as a PostScript name, uses that face's family, and keeps that face's own weight unless a `fontWeight` is set. So `'BrandSans'` plus `fontWeight: '600'` and `'BrandSans-SemiBold'` can both land on the same face.
  • How can you catch a fontFamily name mismatch before users see it in a React Native app?
    Nothing throws, so the check has to be visual or static. Render a type specimen screen and compare screenshots on both platforms in CI, or keep every family name in one typography module and have a script confirm each Android name matches a file in `assets/fonts` and each iOS name matches a font's embedded names.

saying these in an interview costs you the question

  • Android reads the family name from inside the font file, just like iOS.
  • The iOS fontFamily must match the font's file name.
  • A fallback list like 'BrandSans, sans-serif' protects you if the name is wrong.
  • Using the display name from the design tool always works on both platforms.
  • Platform.select is the only way to make one font name portable.