Custom Fonts in React Native WebView: The Complete Fix
A practical guide to loading custom fonts inside React Native WebView. Covers why fonts don't work out of the box, the pitfalls of require() for assets, and the correct platform-specific solution for iOS and Android.
Custom Fonts in React Native WebView: The Complete Fix
You've set up custom fonts in your React Native app. They render perfectly in every Text component. Then you open a WebView and render some HTML — and the font falls back to the default system font.
This isn't a bug you did anything wrong to cause. It's a fundamental architectural constraint of how WebView works. This guide explains exactly why it happens and how to fix it properly for both iOS and Android.
Table of Contents
- Why Custom Fonts Don't Work in WebView
- The Wrong Approaches (And Why They Fail)
- The Correct Solution: Platform-Specific Font Injection
- Complete Implementation
- Key Takeaways
1. Why Custom Fonts Don't Work in WebView
When React Native loads custom fonts — say, Gotham-Book.otf — it registers them with the native text rendering system. On iOS this is CoreText, on Android it's the native font manager. Your Text components can then use those fonts by name via fontFamily.
WebView is a completely isolated browser context. It's essentially a sandboxed browser embedded in your app. It has no awareness of:
- Fonts registered with the native text system
- The React Native bridge
- Your app's asset bundle (unless you explicitly expose them)
So when you write font-family: 'Gotham-Book' in CSS inside a WebView, the browser engine looks through its own font registry, finds nothing, and falls back to the default system font.
Setting font-family in the CSS is necessary but not sufficient. You also need to make the font file itself available to the WebView.
2. The Wrong Approaches (And Why They Fail)
Before arriving at the correct solution, here are two approaches that seem reasonable but don't work.
Approach 1: Just set font-family in CSS
body {
font-family: 'Gotham-Book', sans-serif;
}
This does nothing on its own. The WebView has no idea what Gotham-Book is. It will render with the system default font and silently fall back.
Approach 2: Use require() with RNBlobUtil to read the font file
This feels like the right idea — load the font file, convert it to base64, embed it in a @font-face. But it fails at the first step.
// This does NOT return a file path
const fontPath = require('@assets/fonts/Gotham-Book.otf');
console.log(fontPath); // prints: 29
In React Native, require() for binary assets like fonts returns a numeric module ID, not a file path. Passing that to RNBlobUtil.fs.readFile() results in:
TypeError: Missing argument "path"
Because readFile receives 29 instead of a valid path.
3. The Correct Solution: Platform-Specific Font Injection
The right approach differs between iOS and Android because each platform stores assets differently.
iOS
On iOS, the app bundle is accessible at runtime via RNBlobUtil.fs.dirs.MainBundleDir. Custom fonts linked in Xcode are placed directly in the bundle root, so you can construct the path and read the file as base64.
const fontPath = `${RNBlobUtil.fs.dirs.MainBundleDir}/Gotham-Book.otf`;
const base64Data = await RNBlobUtil.fs.readFile(fontPath, 'base64');
You then embed it using @font-face with a data URL:
@font-face {
font-family: 'Gotham-Book';
src: url('data:font/otf;base64,<base64Data>') format('opentype');
}
Android
On Android, assets are stored in the APK's assets/ directory. You cannot read them with RNBlobUtil.fs.readFile because the file system path doesn't exist — assets inside an APK aren't exposed as regular files.
However, WebView on Android can resolve file:///android_asset/ URLs natively. So instead of reading and base64-encoding the font, you pass the asset URL directly into @font-face:
@font-face {
font-family: 'Gotham-Book';
src: url('file:///android_asset/fonts/Gotham-Book.otf');
}
The WebView engine handles the asset resolution itself.
4. Complete Implementation
Here's the full implementation combining both platform approaches:
import React, { useState, useEffect } from 'react';
import { Dimensions, Linking, Platform } from 'react-native';
import WebView from 'react-native-webview';
import RNBlobUtil from 'react-native-blob-util';
const width = Dimensions.get('window').width - 32;
const HtmlRenderer = (props: { content: string; title: string }) => {
const { content } = props;
const [fontData, setFontData] = useState<string>('');
useEffect(() => {
const loadFont = async () => {
try {
if (Platform.OS === 'ios') {
// iOS: read font file from app bundle and convert to base64
const fontPath = `${RNBlobUtil.fs.dirs.MainBundleDir}/Gotham-Book.otf`;
const base64Data = await RNBlobUtil.fs.readFile(fontPath, 'base64');
setFontData(base64Data);
} else {
// Android: pass the asset URL directly — WebView resolves it natively
setFontData('file:///android_asset/fonts/Gotham-Book.otf');
}
} catch (error) {
console.error('Error loading font:', error);
}
};
loadFont();
}, []);
const fontFaceSrc = fontData.startsWith('file://')
? `url('${fontData}')`
: `url('data:font/otf;base64,${fontData}') format('opentype')`;
const wrappedHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@font-face {
font-family: 'Gotham-Book';
src: ${fontFaceSrc};
}
body {
margin: 0;
padding: 0;
font-family: 'Gotham-Book', sans-serif;
}
img { max-width: 100%; height: auto; }
</style>
</head>
<body>
${content}
</body>
</html>
`;
return (
<WebView
originWhitelist={['*']}
source={{ html: wrappedHtml }}
style={{ width }}
javaScriptEnabled
/>
);
};
export { HtmlRenderer };
What's happening here
fontDatastate holds either a base64 string (iOS) or a file URL (Android)fontData.startsWith('file://')distinguishes the two cases for the@font-facesrc- On iOS, the font is embedded directly in the HTML — no external requests needed
- On Android, the WebView resolves the asset URL using its native asset loader
5. Key Takeaways
-
WebView is isolated. It doesn't inherit any native fonts registered by React Native. You must provide the font file explicitly within the HTML.
-
require() returns a module ID, not a path. Never pass a
require()result for a binary asset to a file system reader. It returns a numeric ID, not a path. -
iOS and Android need different strategies. iOS exposes the app bundle on the file system; Android does not. On Android, let the WebView's native resolver handle the
file:///android_asset/protocol. -
Use
@font-facewithformat('opentype')for.otffiles. The MIME typefont/otfand theformat('opentype')hint ensure browser engines recognise and load the font correctly. -
react-native-blob-utilis your friend for iOS.RNBlobUtil.fs.dirs.MainBundleDirgives you a reliable path to the app bundle, where linked fonts live.
Custom font rendering in WebView is one of those problems that isn't documented clearly anywhere — it requires understanding how both the React Native asset system and the WebView rendering engine work independently. Once you understand the isolation boundary, the solution becomes straightforward.
Let's bring your app idea to life
I specialize in mobile and backend development.
Share this article
Related Articles
🚀 CI/CD for React Native Android: Build, Sign, Version, and Upload to Google Play with GitHub Actions
A complete guide to React Native CI/CD with GitHub Actions: build signed APK/AAB, auto-increment versionCode, and release to Google Play automatically — no more manual Gradle commands or Play Console uploads.
🚀 React Native iOS CI/CD with GitHub Actions — Build and Deploy to TestFlight Automatically
A complete step-by-step guide to automate React Native iOS builds using GitHub Actions: handle code signing, provisioning, Xcode 16 compatibility, and seamless TestFlight uploads — no manual Xcode archives needed.
Building Offline-First React Native Apps: Complete Implementation Guide
Master offline-first architecture in React Native. Learn AsyncStorage, Realm, SQLite, sync strategies, and conflict resolution for seamless offline UX.