Adding a custom font is two decisions: where the file lives, and whether the app may draw before it arrives.
Expo documents two ways to add a custom font, and they differ in when the font exists rather than in how the text looks. The expo-font config plugin embeds the file in your project's native code, so the font is there the moment the app starts. The useFonts hook loads it asynchronously after the app is already running, which is why every example of it is paired with a splash screen. Expo recommends the plugin. Every field name, version number and sentence below comes from Expo's own fonts guide and from React Native's Text documentation, both read on 17 October 2026.
If you were handed this project rather than typing it, the useful question is not how to call a loader. It is which of your screens is allowed to render before the font is ready, and how many separate places the font name has to appear for the app to look like one app. That second number is larger than people expect, and it is the reason a half-finished font change looks worse than no font change at all.
Start hereThe short version
Embed it at build time, or hold the screen until it loads.
Put the file in the repository. Declare it once, either in app.json through the expo-font config plugin or in code through the useFonts hook. Then write the family name into the styles that need it. The plugin wins on everything except compatibility: the font is present at startup, there is no loading code, and the file travels inside the app download. Its price is a native build, which also means it does not work in Expo Go.
The part nobody budgets for is the third step. React Native has no document-wide font rule and no fallback list, so the family name has to reach every piece of text through a style or a shared component. Miss some and those lines keep rendering in the system face, quietly, with no error. People who have been handed a generated app can meet this on day one and think the font failed to load, when in fact it loaded and half the screens never asked for it.
What does Expo actually document about adding a custom font?
Two methods, for both a local file and a Google Font, and the guide presents them as a pair rather than a sequence. First the file. Expo says to copy it into your project's assets/fonts directory, and is careful to call that a common convention rather than a requirement. Font paths in the configuration are relative to the project root. Expo officially supports otf and ttf across Android, iOS and web, and warns that rendering a format a platform does not support may crash the app. In its format table, woff and woff2 are marked unsupported on Android, dfont is Android only, and eot and svg are web only.
The config plugin is the first method. In the words of the guide, it allows embedding one or more font files in your project's native code. You add expo-font to the plugins array in app.json with a fonts list, where each entry is either a path string or an object carrying fontFamily, weight and style. There are optional android and ios keys when the two platforms need different lists, and on Android a family is described with a fontFamily plus a fontDefinitions array, each face needing a unique weight and style pair. Expo gives three reasons to prefer it: the fonts are available immediately when the app starts on a device, no extra code is required to load them asynchronously, and they are consistently available across every device where the app is installed because they are bundled in. After embedding one, Expo tells you to create a new development build and install it on a device, an emulator or a simulator.
The useFonts hook is the second. It loads the font file asynchronously from a top-level component, taking a map of family names to files, and returning a loaded flag and an error value that the API reference describes as being for use in development. Its advantages are the mirror image of the plugin's: it works with all Expo SDK versions and with Expo Go, and it is the method Expo points to for web, because the plugin covers Android and iOS only. The same page also shows the hook being handed Ionicons.font from the vector icons package, which is worth noticing. Your icons are a font too, and they are usually on the first screen.
Neither method is a styling choice, which is the thing to take away before touching the project. One is a build configuration and one is application code. If you are reading this because someone else set the project up, you can tell which you have by opening app.json and looking for expo-font in the plugins array. If it is not there, the fonts are either being loaded in code or not being loaded at all. That same distinction is worth understanding before building React Native apps without coding starts producing changes you have to review.
Expo, fonts, modified 23 September 2026, read 17 October 2026
Check yours
Which method your situation points at
Tick what is true of your project. Each answer is the one Expo gives for that constraint, not a preference.
Nothing ticked. With no constraints at all, Expo calls the config plugin the recommended method. The hook is the one that works with every SDK version and with Expo Go.
Which fonts have to be there before the first frame?
Expo answers this twice, once for each method, and the two answers are the clearest statement of the trade-off anywhere in the guide. For the config plugin: fonts are available immediately when the app starts on a device. The Google Font version of the same sentence says the fonts are bundled with the app download and will be available immediately. There is nothing to wait for, because the waiting happened when the build was made.
For the runtime hook, the documented answer is the splash screen, and it is not optional decoration. Expo's example imports expo-splash-screen, calls preventAutoHideAsync at the top of the module so the screen stays up, renders nothing at all while the fonts are neither loaded nor errored, then calls hideAsync once one of the two is true. Expo describes the purpose in plain words: to prevent rendering the app until the font is loaded and ready. The error branch matters as much as the loaded branch, because a font that fails is still a splash screen you have to take down.
Here is where the documentation stops, and it is worth being exact about it rather than filling the gap with folklore. On the fonts guide, read on 17 October 2026, there is no sentence describing what the screen looks like if you render before the font is ready. The phrases people repeat, a flash of unstyled text or a flicker, do not appear. Expo tells you how to avoid the moment and does not characterise it. We looked for that and could not find it, so treat any specific claim about what users see as somebody's observation rather than a documented behaviour.
The practical reading is that the list of fonts needed before the first frame is the list of faces that appear on your first screen, your icon font included, and that a splash screen held open for them is time the user spends looking at your logo. That cost belongs in the same budget as everything else in app startup time, which is the argument for embedding at build time on the two platforms that allow it and keeping the hook for web.
Why does one font file end up with two different names?
Because the two platforms read the name from different places. When you give the plugin an array of file paths, Expo documents that Android takes the family name from the file name without its extension, while iOS takes it from the name stored inside the font file. Those are frequently not the same string. Expo's recommended fix is to name the file after its PostScript name, so that the family name is consistent on both platforms, and it offers Platform.select for the cases where you cannot. If you use the object form instead, the name to write is the family name, which Expo says you can read in Font Book or a tool such as fontdrop.info.
The Google Fonts packages make the split visible. For the same weight of the same family, Expo's example writes Inter_900Black on Android, which is the file name, and Inter-Black on iOS, which is the PostScript name. A style that hardcodes one of those is half broken and will look fine to whoever tested it on one simulator. This is the single most common reason a font change appears to work and then does not.
What is missing from both sources is the failure case. Neither Expo's fonts guide nor React Native's text style props reference says what happens when a fontFamily string names a font that is not loaded. No error, no warning, no fallback rule is described on either page, and we could not find one as of 17 October 2026. The adjacent case is documented: Expo says that if you do not specify a fontFamily, the platform default font is used, and that the default is Roboto on Android and SF Pro on iOS. So the thing to plan for is text that still draws, in a face you did not pick, with nothing on the screen telling you which of the two problems you have.
That is why the only reliable check for a font change is to look at both platforms. Not a build log, not a passing test. Open the app on an Android device and an iOS device and read the screens, because the bug here renders successfully.
What goes wrong when only some of the text changes?
React Native is explicit that there is no document-wide font. Its Text documentation says you must wrap all text nodes inside a Text component, that you cannot put a text node directly under a View, and, in its own words, that you also lose the ability to set up a default font for an entire subtree. Style inheritance exists, but the docs say it is limited to text subtrees: a nested Text inherits from the Text around it, and nothing inherits from a View. The stated reason is isolation between components, and properties arriving from outside the props would break it.
The second half of the problem is that there is no safety net in the value either. React Native says fontFamily only accepts a single font name, which is different from font-family in CSS. No list, no second choice, no generic family to fall back to except the handful of system families iOS recognises. One string, right or wrong. Put those two facts together and a font change is a sweep through every style object in the project, which is precisely the job that goes half done in a codebase you did not write and cannot hold in your head.
React Native's own answer is a component. The docs recommend creating one, which they call MyAppText, that carries the font and the size, and using it across the application instead of repeating the style. For someone maintaining generated screens this is the highest-value refactor available: it turns a font change from a search and replace into a one-line edit, and it makes the next change cheap too. Do it before you change the font, not after.
Weights are the last trap, and both sources contribute. React Native says normal and bold are supported for most fonts, but that not all fonts have a variant for each numeric value, in which case the closest one is chosen. Expo says to declare every face of a family in one call, with weight and style given for each face, and that loading several files under one family name arrived in SDK 58, as did variable font support on Android and iOS. The Android details are specific: the weight axis of a variable font needs Android 10 or later, the italic axis needs Android 15, and on earlier versions the font renders at its default weight while Android fakes the bold by thickening the letters. If a bold heading looks slightly wrong rather than badly wrong, that is usually what you are seeing, not the Hermes and the new architecture layer underneath.
React Native, Text, limited style inheritance, read 17 October 2026
The two methods, side by side
| The question | expo-font config plugin | useFonts hook | Same either way |
|---|---|---|---|
| Where you declare the font | app.json, inside the expo-font plugin entry | In code, in a top-level component | No |
| When the font exists | Immediately when the app starts | After the hook resolves at runtime | No |
| Font loading code in the app | No | Yes | No |
| Needs a new native build | Yes | No | No |
| Works in Expo Go | No | Yes | No |
| Covers the web platform | No | Yes | No |
| otf and ttf accepted | Yes | Yes | Yes |
| Name you write in fontFamily | File name on Android, the name inside the file on iOS | The key you mapped it to, with the same split underneath | No |
Rows taken from Expo's fonts guide and its expo-font API reference, read 17 October 2026. Version-dependent rows are called out in the text above.
Where this lands if a builder generated your app
Nothing about the font changes because an agent wrote the screens. The file still has to be in the repository, the family name still has to be right on two platforms, and somebody still has to decide which screens may draw before it is ready. What does change is who is holding the two files where the answer lives, app.json and whichever top-level component runs first.
Newly is an AI app builder. You describe an app in plain English and it generates a real React Native and Expo project that you own, with two-way GitHub sync, so the plugins array and the font file are yours to edit in the normal way. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It is $25 a month, with no free tier. The reason that matters here is narrow but real: the config plugin's price is a native build, and a project that is already producing builds has already paid it.
The honest limits are worth stating too. No builder can licence your brand font for you, and no builder can know that the family name inside the file you were sent differs from its file name. And on iOS, Newly does not submit for you, because Apple requires apps made with an app generation service to be submitted by the provider of the content. The build carrying your embedded font is one you push through yourself, which is the same step as publishing to the App Store with any other React Native project.
So the sequence for a generated app is the same as for a hand-built one, in a different order. Ask the agent for a shared text component first. Add the font file and declare it once. Point the component at the family name, with the platform split handled in one place. Then look at both platforms with your eyes. A font is the one change where every automated check passes and the app still looks wrong.
Questions people ask about adding a custom font
Expo documents two ways. The expo-font config plugin embeds one or more font files in your project's native code, declared in app.json, and Expo calls it the recommended method. The useFonts hook from the same library loads the file asynchronously at runtime from a top-level component. The file itself usually goes in an assets/fonts directory, which Expo describes as a convention rather than a requirement, and font paths are relative to the project root. Read on 17 October 2026 from Expo's fonts guide, which carries a modified date of 23 September 2026.
Describe the app, then own the two files that hold the font
The font is not the hard part. Deciding which screens may draw without it, and keeping one name in one place, is the part that still needs a person. Both live in a project you can open.
Start building