Custom Fonts in React Native WebView: The Complete Fix

6 min read
#React Native#WebView#Fonts#iOS#Android#Mobile Development

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

  1. Why Custom Fonts Don't Work in WebView
  2. The Wrong Approaches (And Why They Fail)
  3. The Correct Solution: Platform-Specific Font Injection
  4. Complete Implementation
  5. 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

  • fontData state holds either a base64 string (iOS) or a file URL (Android)
  • fontData.startsWith('file://') distinguishes the two cases for the @font-face src
  • 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

  1. WebView is isolated. It doesn't inherit any native fonts registered by React Native. You must provide the font file explicitly within the HTML.

  2. 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.

  3. 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.

  4. Use @font-face with format('opentype') for .otf files. The MIME type font/otf and the format('opentype') hint ensure browser engines recognise and load the font correctly.

  5. react-native-blob-util is your friend for iOS. RNBlobUtil.fs.dirs.MainBundleDir gives 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