fix(storybook-rn): load custom fonts at runtime for Expo Go compatibility#1186
Conversation
…lity Switch from expo-font config plugin (build-time) to useFonts hook (runtime) so Geist, MM Sans, and MM Poly render correctly when running via Expo Go without a local native build.
📖 Storybook Preview |
Fonts are now loaded at runtime via useFonts in index.tsx. The config plugin only applies to native builds and is a no-op in the current Expo Go workflow.
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: expo-font plugin removed despite stated intent to keep
- Restored the expo-font config plugin in apps/storybook-react-native/app.json with all existing font files to ensure native builds embed fonts.
Or push these changes by commenting:
@cursor push 45d8f4430a
Preview (45d8f4430a)
diff --git a/apps/storybook-react-native/app.json b/apps/storybook-react-native/app.json
--- a/apps/storybook-react-native/app.json
+++ b/apps/storybook-react-native/app.json
@@ -16,6 +16,24 @@
"backgroundColor": "#ffffff"
}
},
- "plugins": []
+ "plugins": [
+ [
+ "expo-font",
+ {
+ "fonts": [
+ "./fonts/Geist/Geist-Regular.otf",
+ "./fonts/Geist/Geist-RegularItalic.otf",
+ "./fonts/Geist/Geist-Medium.otf",
+ "./fonts/Geist/Geist-MediumItalic.otf",
+ "./fonts/Geist/Geist-SemiBold.otf",
+ "./fonts/Geist/Geist-SemiBoldItalic.otf",
+ "./fonts/MMPoly/MMPoly-Regular.otf",
+ "./fonts/MMSans/MMSans-Regular.otf",
+ "./fonts/MMSans/MMSans-Medium.otf",
+ "./fonts/MMSans/MMSans-Bold.otf"
+ ]
+ }
+ ]
+ ]
}
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit e0e7f70. Configure here.
| }); | ||
|
|
||
| function App() { | ||
| const [fontsLoaded] = useFonts({ |
There was a problem hiding this comment.
Font loading lives here at the app root rather than in the Storybook preview.tsx decorator by design. A decorator re-runs per story, which can cause timing issues on Android where font registration isn't guaranteed to be complete before the first render. Loading once at startup — before StorybookUIRoot mounts — ensures fonts are always available. See useFonts docs.
|
|
||
| function App() { | ||
| const [fontsLoaded] = useFonts({ | ||
| 'Geist-Regular': require('../fonts/Geist/Geist-Regular.otf'), |
There was a problem hiding this comment.
The keys in this map are the PostScript names of the font files, which is how iOS and Android register and look up custom fonts at runtime. These must match exactly — 'Geist-SemiBold' is the PostScript name of Geist-SemiBold.otf, not a display name. The @metamask/design-system-twrnc-preset typography config maps Tailwind font classes (e.g. font-default-bold) to these same PostScript names, which is how Text component font weights resolve correctly. See expo-font loadAsync.
| }); | ||
|
|
||
| if (!fontsLoaded) { | ||
| return null; |
There was a problem hiding this comment.
Returning null while fonts are loading prevents stories from rendering with the system fallback font (San Francisco on iOS, Roboto on Android) before custom fonts are ready. The blank screen is brief since font files are served locally via Metro. For a production app, expo-splash-screen would be the recommended approach to hold the splash screen during this window, but that's unnecessary overhead for a dev-only Storybook app. See Expo font loading guidance.
| } | ||
| ] | ||
| ] | ||
| "plugins": [] |
There was a problem hiding this comment.
The expo-font config plugin runs during expo prebuild and modifies native project files — it adds font filenames to UIAppFonts in Info.plist on iOS and copies them into the Android assets directory. It is a build-time operation that only takes effect in a locally-compiled native app. Since the project runs via Expo Go, a pre-built binary distributed by Expo, this plugin was silently ignored and fonts were never registered with the OS. Fonts are now loaded at runtime via useFonts in index.tsx instead. See Expo config plugins.
📖 Storybook Preview |


Description
Custom fonts (Geist, MM Sans, MM Poly) were not rendering in the React Native Storybook app — all text fell back to the system font regardless of the
fontFamilyorfontWeightprop applied via theTextcomponent.Background
Prior to #1165, the Storybook app used native builds (
expo run:ios/expo run:android), which compiled and installed a custom native binary. In that workflow, theexpo-fontconfig plugin inapp.jsonworked correctly — it embedded font files into the native binary at build time by modifyingInfo.plist(UIAppFonts) and the Xcode/Gradle project.#1165 aligned Expo and React Native dependencies with MetaMask Mobile and switched the Storybook app scripts back to Expo Go (
expo start --ios/expo start --android). This is what surfaced the font regression.Root cause
The
expo-fontconfig plugin approach only works when running a locally-built native app. Since the project now runs via Expo Go — a pre-built binary distributed by Expo — project-level native plugins are silently ignored. When React Native tried to usefontFamily: 'Geist-Medium', iOS/Android found no registered font with that PostScript name and silently fell back to the system font.Fix
Switch to runtime font loading via the
useFontshook fromexpo-font, which loads fonts as Metro assets at app startup. This works in Expo Go because it uses JavaScript-layer APIs rather than native plugin execution.Font loading is placed in the root entry component (
index.tsx) rather than a Storybook decorator so fonts are guaranteed to be available before any story renders — loading in a per-story decorator can cause timing issues on Android.Changes:
.rnstorybook/index.ts→index.tsxand added anAppwrapper component that loads all fonts viauseFontsbefore renderingStorybookUIRootpackage.jsonmainfield accordinglyexpo-fontconfig plugin fromapp.jsonRelated issues
Fixes:
Manual testing steps
yarn storybook:iosand open the Text → Font Weight story — confirm Regular, Medium, and Bold render visibly different weightsyarn storybook:androidand repeat steps 1–2Screenshots/Recordings
Before
All three font weights and all three font families rendered identically in the system font (San Francisco on iOS, Roboto on Android).
before720.mov
After
All three font weights and all three font families rendered as expected
after720.mov
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Low Risk
Storybook-only startup and font loading changes with no production app, auth, or data impact.
Overview
The React Native Storybook entry point now loads Geist, MM Sans, and MM Poly at runtime with
useFontsinstead of relying on theexpo-fontconfig plugin, so custom typefaces work when the app runs in Expo Go (native plugins are not applied there).The root registers an
Appwrapper that waits for fonts before renderingStorybookUIRoot, the entry file is renamed toindex.tsxwithpackage.jsonmainupdated, and the redundantexpo-fontplugin block is removed fromapp.json.Reviewed by Cursor Bugbot for commit 63cc501. Bugbot is set up for automated code reviews on this repo. Configure here.