RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-04-22Intermediate

Loading Custom Fonts Cleanly in Rork Apps — Preventing Startup Flicker (FOIT/FOUT) With expo-font

When you ship a Rork-generated app with a custom font, you often see a brief flash of the system font at launch — the classic FOIT/FOUT problem. This guide shows how to combine expo-font and expo-splash-screen to hold the splash screen until fonts are ready, with copy-paste-ready code.

Rork515expo-fontCustom FontsSplashScreenFOITFOUTReact Native209

When you first run a Rork-generated app on a real device, you may notice a subtle but annoying visual hitch: for a split second, every piece of text renders in the system font (San Francisco on iOS, Roboto on Android), and only then switches to your custom font. It's the mobile equivalent of FOIT (Flash of Invisible Text) and FOUT (Flash of Unstyled Text) that web developers have been fighting for years.

Telling Rork "use Noto Sans JP please" in a prompt is not enough to fix this. The flash comes from a timing gap between React rendering and font loading, so you have to control when the splash screen disappears. In this article, I'll walk through the pattern I reach for every time: hold the splash screen until fonts are ready, then hide it by hand. It's not glamorous, but it's the difference between an app that feels polished and one that feels rushed.

What's actually happening during those 200ms

A Rork project runs on Expo and React Native, so the startup pipeline roughly looks like this:

  1. The native side shows the splash screen.
  2. The JavaScript bundle loads and evaluates React.
  3. Hooks like useFonts kick off asynchronous font loading.
  4. React renders the root component.
  5. Fonts finish loading; text is re-rendered with the custom font.

The flicker lives between step 4 and step 5. If React renders before fonts are ready, the first paint uses the fallback system font, then swaps when the custom font arrives. The user perceives this as a flash.

Setting fontFamily in a Rork prompt doesn't solve this — you need to control rendering timing itself.

The fix in one sentence — keep the splash up until fonts are ready

By default, Expo hides the splash screen as soon as your root component mounts. We're going to switch that to manual mode: prevent auto-hide, wait for useFonts to resolve, and only then hide the splash. No splash-to-content gap means no flicker.

Install the two packages. Rork projects typically already have expo-font, but expo-splash-screen often needs to be added explicitly.

# Run at the root of your Rork project
npx expo install expo-font expo-splash-screen

You can ask Rork in chat to rewrite your entry file for this pattern, and it will produce something close to what I show below. That said, I've learned not to hand off the small details entirely to the AI — a few edge cases catch you off guard. Let's walk through the minimal robust version.

A working _layout.tsx you can drop in

If your Rork project uses Expo Router (the default), the cleanest place for this logic is app/_layout.tsx. Here's a version I've shipped in production several times.

// app/_layout.tsx
import { Stack } from "expo-router";
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { View } from "react-native";
 
// 1. Prevent the splash from auto-hiding.
//    Without this, React mounts and the splash disappears immediately.
SplashScreen.preventAutoHideAsync().catch(() => {
  // Safe to ignore in hot-reload or WebView contexts.
});
 
export default function RootLayout() {
  // 2. Register your custom fonts.
  const [fontsLoaded, fontError] = useFonts({
    "NotoSansJP-Regular": require("../assets/fonts/NotoSansJP-Regular.ttf"),
    "NotoSansJP-Bold": require("../assets/fonts/NotoSansJP-Bold.ttf"),
  });
 
  // 3. Hide the splash only when fonts are loaded OR errored.
  useEffect(() => {
    if (fontsLoaded || fontError) {
      SplashScreen.hideAsync();
    }
  }, [fontsLoaded, fontError]);
 
  // 4. Render nothing until fonts are ready — this is the key to no flicker.
  if (!fontsLoaded && !fontError) {
    return null;
  }
 
  return (
    <View style={{ flex: 1 }}>
      <Stack screenOptions={{ headerShown: false }} />
    </View>
  );
}

The critical bit is step 4: returning null until fonts are ready. By deferring the first render, we physically remove the window where the system font could show through. Including fontError in the guard means that even if a font file is broken, the app still moves past the splash instead of hanging forever.

What you should observe

  • Startup takes slightly longer than before (roughly font load + 100–300ms).
  • The moment the splash disappears, your first screen is already painted in the custom font.
  • If font loading fails, the splash still hides — you just fall back to system fonts gracefully.

Edge cases that have bitten me

1. Wrong require path

A typo like assets/font/ vs assets/fonts/ will crash Metro with Unable to resolve module. More commonly, the file name casing or hyphenation doesn't match — Google Fonts ships NotoSansJP-Regular.ttf, so match that exact string both in your useFonts key and in the file name.

2. Using the filename (with extension) as fontFamily

A common mistake is <Text style={{ fontFamily: "NotoSansJP-Regular.ttf" }}>. The correct value is the key you passed to useFonts, without the extension:

<Text style={{ fontFamily: "NotoSansJP-Regular", fontSize: 16 }}>
  Hello, custom-font world
</Text>

When you ask Rork to "render this text in Noto Sans JP Regular", it occasionally produces the extension-included version. A quick grep -n "\.ttf\"" app/ after generation catches this before it hits a device.

3. iOS caching an old font file

If you swap out a font during development and the simulator still shows the old one, the culprit is almost always a cache. In order:

  • Clear Metro: npx expo start --clear
  • Delete the app from the simulator's home screen and reinstall
  • If you're on a bare workflow, wipe ios/build and android/build and rebuild

4. Splash never hides

If useFonts stays stuck at [false, undefined], nine times out of ten it's a path resolution problem. expo-font raises an error internally, but your useEffect only reacts if fontError is set. Add temporary logging until you can reproduce it:

useEffect(() => {
  if (fontsLoaded) console.log("[Font] loaded OK");
  if (fontError) console.log("[Font] error:", fontError);
  if (fontsLoaded || fontError) {
    SplashScreen.hideAsync();
  }
}, [fontsLoaded, fontError]);

5. Fonts look correct in dev but wrong in production

If your dev build shows the right font but the EAS-built production binary shows the system font, check that your font files are included in the build. Specifically, confirm:

  • Font files are inside assets/fonts/ (not src/assets/fonts/ or anywhere custom).
  • Your app.json or app.config.js does not have an assetBundlePatterns entry that excludes the font folder.
  • You did not accidentally .gitignore the fonts, leaving them missing on the CI runner.

I once spent an afternoon debugging this only to realize my teammate had added assets/fonts/*.ttf to .gitignore after running a font-license check script. The CI build had no fonts at all. A simple git ls-files assets/fonts/ saved future me.

How to phrase this in a Rork prompt

When you want Rork to generate this setup in one shot, the prompt needs four facts:

  • Which font family and weights you want
  • Where you placed the font files
  • That you want expo-splash-screen to hold until fonts load
  • That the logic should live in _layout.tsx

Here's the template I keep in my notes:

I placed Noto Sans JP Regular and Bold in assets/fonts/. In app/_layout.tsx, load them with useFonts, prevent expo-splash-screen from auto-hiding, and only hide the splash once fonts are loaded. Return null before fonts are ready so we don't see a FOUT flash.

If the result isn't quite right, I've found it's more effective to add one small correction at a time ("also include fontError in the guard", "swallow the preventAutoHideAsync error") rather than rewriting the whole prompt. Small, iterative nudges give Rork the best context.

Cutting bundle size when your font is Japanese or CJK

Noto Sans JP is wonderful, but the full Regular file ships around 5 MB. Shipping two weights (Regular + Bold) adds ~10 MB to your app bundle. For an app that's otherwise only 20–30 MB, that's a meaningful hit on install size and first-launch download.

Two practical mitigations have worked for me:

Subset the font to the glyphs you actually use. If your app is primarily Japanese with a known vocabulary (say, a meditation app with a fixed set of phrases), tools like pyftsubset (part of fonttools) can cut the font file down to a tenth of its original size. You keep only the glyphs that appear in your app copy.

# Extract glyphs from your translation files into a unicode list,
# then run pyftsubset to create a trimmed font.
pip install fonttools
pyftsubset NotoSansJP-Regular.ttf \
  --unicodes-file=used_unicodes.txt \
  --output-file=NotoSansJP-Regular.subset.ttf

Swap the file in assets/fonts/ with the subset version and your app suddenly ships in a much smaller package. Just remember to re-run the subsetter whenever you add new user-visible strings.

Lazy-load the font for offscreen content. If your app has a rarely used screen (say, a legal notices page) that needs a different weight, you can fetch the font from your CDN at runtime rather than bundling it. expo-font supports loading from a URL via Font.loadAsync({ "FontName": { uri: "https://..." } }). This pattern costs you a brief loading state on the destination screen, but it keeps your initial bundle lean.

A few patterns I've settled on after shipping several apps

Over several Rork projects I've converged on a few small conventions that keep the font story boring and predictable:

  • Put every custom font in assets/fonts/ — no subfolders. This keeps require paths short and reviewable at a glance.
  • Use only two weights (Regular and Bold) unless the design genuinely calls for more. Every extra weight adds megabytes, and most mobile UIs look cleaner with fewer weights anyway.
  • Wrap every Text in an AppText from day one, even if the first version just forwards props. You'll thank yourself the first time you want to change the default font or color globally.
  • Keep the font loading logic in _layout.tsx only. I've seen projects scatter useFonts across several screens, and it always ends in tears when one screen forgets to guard rendering on fontsLoaded.

A small extra tip for Android

On iOS, Metro bundles your fonts reliably. On Android — especially on some Chinese-market devices running heavily modified ROMs — I've seen flaky fontFamily resolution. A defensive wrapper around Text solves it cleanly:

import { Text, TextProps } from "react-native";
 
export function AppText(props: TextProps) {
  return (
    <Text
      allowFontScaling={true}
      {...props}
      style={[
        { fontFamily: "NotoSansJP-Regular", color: "#222" },
        props.style,
      ]}
    />
  );
}

Using <AppText> everywhere instead of the raw <Text> means a future font swap becomes a one-line change.

What to try next

Open your current Rork project, paste the _layout.tsx snippet, and relaunch the simulator. The difference in feel is immediate — the app stops announcing itself as "an Expo app" and starts feeling like your app. From there, unifying typography across the whole app is what turns Rork's "good enough" UI into something you're actually proud to ship.

If you want to keep leveling up the polish of Rork-generated UIs, I've written about related tactics in Fixing the "cheap look" of Rork-generated UIs and Animations in Rork apps.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
Dev Tools2026-07-14
Designing Seams That Survive AI Regeneration in Rork
Every follow-up prompt to Rork can quietly wipe out logic you wrote by hand. Protecting it with prompts is a patch, not a fix. Here is how to separate generated code from code you own, and draw a boundary that regeneration cannot reach, with working Zustand and service-layer examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →