Shipnative
ShipnativeShipnative
Sign in
GuideSeptember 2026 · 8 min read

React Native Custom Fonts: Load Them Without the Flash

Fonts are the fastest way to make a generated app stop looking generated, and the fastest way to make it look broken. Three things go wrong, always in the same order: the app renders in the system font for a beat before yours arrives, then fontWeight: 'bold' quietly does nothing on Android, then half the app still shows the default because a library rendered its own Text. Here’s the setup that avoids all three, and the reason each one happens.

Four ways to get a font in, ranked

MethodLoad flashWorks in Expo GoUse it for
expo-font config plugin (native embed)None — font is in the binaryNo — needs a buildAnything you ship
useFonts hook (runtime)Yes, unless you hold the splashYesPrototyping, Expo Go, fonts chosen at runtime
@expo-google-fonts/* packageSame as useFontsYesGoogle Fonts without hunting for files
System font stackNoneYesWhen the brand does not depend on type

The honest recommendation is to use both, in that order over the life of the project: the hook while you’re still in Expo Go and iterating on which typeface you even want, then the config plugin the moment you start producing real builds. Switching is a five-line change and it deletes an entire class of launch bug.

One family name per weight

This is the part almost everyone gets wrong first. In a browser you register one family and pick weights with font-weight. On Android that does not work for custom families: it looks for a registered font matching the requested weight, doesn’t find one, and falls back to the single cut you loaded. There is no synthetic bolding. So register each cut as its own family:

// assets/fonts/
//   Inter-Regular.ttf
//   Inter-Medium.ttf
//   Inter-SemiBold.ttf
//   Inter-Bold.ttf

// theme/type.ts — the only place font names are written as strings
export const fonts = {
  regular: 'Inter-Regular',
  medium: 'Inter-Medium',
  semibold: 'Inter-SemiBold',
  bold: 'Inter-Bold',
} as const;

// Usage: pick the family, not the weight.
//   GOOD: { fontFamily: fonts.bold }
//   BAD:  { fontFamily: fonts.regular, fontWeight: '700' }   // no-op on Android

Ship only the weights you use. Four cuts of a typical variable-source family is roughly a megabyte of binary you carry forever, and most apps genuinely need two: a body weight and a heading weight. Adding italic doubles the count again, so decide whether you actually use it.

Runtime loading, without the flash

If you load fonts at runtime, the app can render before they arrive. The fix is to keep the splash screen up until the hook says it’s done, so the first painted frame is already correct:

// app/_layout.tsx
import { useCallback } from 'react';
import { View } from 'react-native';
import { Stack } from 'expo-router';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';

// Module scope — must run before the first render.
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [loaded, error] = useFonts({
    'Inter-Regular': require('../assets/fonts/Inter-Regular.ttf'),
    'Inter-Medium': require('../assets/fonts/Inter-Medium.ttf'),
    'Inter-Bold': require('../assets/fonts/Inter-Bold.ttf'),
  });

  const onReady = useCallback(async () => {
    // Hide once we have painted, so there is no blank frame between the two.
    if (loaded || error) await SplashScreen.hideAsync();
  }, [loaded, error]);

  // Note: continue on error. A missing font should not be a black screen.
  if (!loaded && !error) return null;

  return (
    <View style={{ flex: 1 }} onLayout={onReady}>
      <Stack />
    </View>
  );
}

The error branch is not decoration. If a font file is missing or corrupt in a production build, gating purely on loaded gives every user a permanent splash screen — the worst possible failure for the most cosmetic possible feature. Render in the fallback font and log it instead. The splash screen guide covers the handoff timing in more detail.

Embedding natively: no loading state at all

Once you are producing development or production builds, the fonts can go into the binary and be available on the very first frame. Configure the expo-font plugin and rebuild:

// app.json
{
  "expo": {
    "plugins": [
      [
        "expo-font",
        {
          "fonts": [
            "./assets/fonts/Inter-Regular.ttf",
            "./assets/fonts/Inter-Medium.ttf",
            "./assets/fonts/Inter-Bold.ttf"
          ]
        }
      ]
    ]
  }
}

Two things to know. This changes native config, so it needs a new build — it will not take effect over an OTA update or in Expo Go. And the family name the platform registers comes from the font file itself rather than from a key you chose, so if text stays stubbornly in the system font after a rebuild, the name in your styles probably doesn’t match the file’s internal name. Check exact spelling and hyphenation before assuming the plugin failed. Consistent filenames that match the internal names save you the trip.

Applying it everywhere without a global patch

The tempting move is to assign Text.defaultProps.styleonce and be done. Don’t: it is deprecated for function components, it breaks silently on upgrades, and it still misses text rendered inside libraries. Shared components are more code the first day and less code every day after:

// components/Type.tsx
import { Text, type TextProps, StyleSheet } from 'react-native';
import { fonts } from '@/theme/type';

const styles = StyleSheet.create({
  body:    { fontFamily: fonts.regular,  fontSize: 16, lineHeight: 23 },
  label:   { fontFamily: fonts.medium,   fontSize: 14, lineHeight: 20 },
  heading: { fontFamily: fonts.bold,     fontSize: 28, lineHeight: 33, letterSpacing: -0.5 },
});

export const Body = (p: TextProps) => <Text {...p} style={[styles.body, p.style]} />;
export const Label = (p: TextProps) => <Text {...p} style={[styles.label, p.style]} />;
export const Heading = (p: TextProps) => <Text {...p} style={[styles.heading, p.style]} />;

Set lineHeightexplicitly while you’re here. Custom fonts carry their own default metrics, and the same numeric size can sit noticeably tighter or looser than the system font did — which is why type often looks subtly wrong right after switching, even though nothing about the sizes changed. Vertical centering inside buttons is where it shows first.

The traps

  1. fontWeight on a custom family. No-op on Android, works on iOS, therefore ships. Select the family instead.
  2. Gating render on loaded alone. A missing file becomes a permanent splash screen in production. Continue on error.
  3. Loading eight weights. Every cut is binary size on a device that already has perfectly good system fonts. Two weights covers most designs.
  4. Assuming the plugin works in Expo Go. Native embedding needs a development build; the hook is what works in Expo Go.
  5. Ignoring the user’s text size setting. Custom type does not exempt you from accessibility scaling. Test at the largest system text size before shipping — layouts that only work at the default size are a common App Review comment.

The shortcut: name the typeface, not the plumbing

Typography is the highest-leverage change you can make to how an app feels, and the setup is entirely mechanical: the same file placement, the same per-weight naming, the same splash handoff, in every project.

Tell ShipNative what you want — “use Inter for body and Space Grotesk for headings” — and it wires the loading, the per-weight families and the shared text components, then runs the app on your phone so you can judge the type at real size instead of in a browser. Export the full Expo project whenever you want it.

Build it free

Describe your app in one sentence and have it running on your own phone in minutes at shipnative.dev. No credit card, full React Native source export whenever you want it.

Frequently Asked Questions

How do I add a custom font to an Expo app?

Drop the .ttf or .otf files into assets/fonts, then either load them at runtime with the useFonts hook from expo-font, or embed them natively by listing the files in the expo-font config plugin in app.json and rebuilding. Runtime loading works in Expo Go and needs no rebuild; native embedding removes the loading state entirely and is the better choice once you are producing development or production builds.

Why does fontWeight: bold do nothing with my custom font on Android?

Android does not synthesize weights for a custom family — it looks for a real font file registered under that name and, finding only the regular cut, renders the regular cut. iOS is more forgiving, which is why this ships. The fix is to load every weight as its own family name (Inter_400Regular, Inter_700Bold) and select the family rather than setting fontWeight.

Why do I see the default system font for a moment before mine appears?

Because the app rendered before the fonts finished loading. Hold the splash screen with SplashScreen.preventAutoHideAsync() at module scope and only hide it once the font hook reports loaded, so the first frame the user sees is already correct. Embedding the fonts natively avoids the problem entirely — there is nothing to wait for.

How do I apply a custom font to the whole app?

Do not patch Text.defaultProps — it is deprecated for function components and it silently misses text rendered by libraries. Export your own Text wrapper that merges a base style, use it everywhere, and give buttons, inputs and any third-party component an explicit style. A handful of shared text components is less code than the global patch and does not break on upgrade.

Can an AI app builder set up fonts for me?

Yes. Say which typeface you want — "use Inter for body text and Space Grotesk for headings" — and ShipNative wires the loading, the weight-per-family naming, and the shared text components, then runs the app on your phone so you can see the type at real size rather than in a browser preview.

→

Expo Splash Screen

The screen you hold while fonts load — and how to hand off without a flash.

Read guide →
→

React Native Dark Mode

The other half of a theme layer: colors that follow the system.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.