Skip to content

Font

Use a custom font without touching the native projects: give a name and a font file, wait for the load, then reference the name in fontFamily. @symbiote-native/font wraps expo-font so every SymbioteNative adapter can do it, not just React. A font source is a require() module id, a URI, or an @symbiote-native/asset Asset. renderToImageAsync (text to image) comes with it on iOS and Android.

OS platform Support
iOS live
Android live
Framework adapter Support
React live
Vue live
Angular live
Svelte live
Solid live
Terminal window
npm install @symbiote-native/font

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --font (or add --font in an existing app) installs and wires this for you - see @symbiote-native/cli.

expo-font and expo-modules-core come along as regular dependencies, pinned to exact versions. Never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

Each adapter has a thin lifecycle wrapper over the same core functions: it seeds its state synchronously from isFontMapLoaded, calls loadAsync once on mount, and never reloads when the font map changes. Render nothing (or a placeholder) until loaded is true, so no text paints in a fallback font first.

import { useFonts } from '@symbiote-native/font/react';
export default function App() {
const [loaded, error] = useFonts({
'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
});
if (!loaded) return null;
return <text style={{ fontFamily: 'Inter-Regular' }}>Hello</text>;
}

Outside a component, call loadAsync directly, for example at startup before the first render:

import { loadAsync } from '@symbiote-native/font';
await loadAsync({ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf') });
Signature Description
isLoaded(fontFamily): boolean Whether the font family has finished loading
getLoadedFonts(): string[] Every font family loaded so far
isLoading(fontFamily): boolean Whether the font family is loading right now
isFontMapLoaded(map): boolean Whether every family in a font map (or the one family named by a string) is already loaded
loadAsync(fontFamilyOrFontMap, source?): Promise<void> Loads one family (name, source) or a whole map of name -> source
unloadAsync(fontFamilyOrFontMap, options?): Promise<void> Always throws UnavailabilityError on iOS and Android. See Notes
unloadAllAsync(): Promise<void> Always throws UnavailabilityError on iOS and Android. See Notes
renderToImageAsync(glyphs, options?): Promise<IRenderToImageResult> Renders a string with a loaded font to an image file and resolves its uri, width and height
Adapter Entry point Description
React useFonts(map) Returns [loaded: boolean, error: Error | null]
Vue useFonts(map) Returns { loaded, error } as refs
Svelte useFonts(map) Returns an object with reactive loaded and error
Solid createFonts(map) Returns { loaded(), error() } as accessors
Angular inject(FontsService).connect(map) Returns { loaded, error } as signals

Every entry point takes the same argument: Record<string, FontSource>, a map from the fontFamily name you will use in styles to the font file.

  • The fontFamily in a style must match the map key exactly. 'Inter-Regular' in the map and fontFamily: 'Inter' in the style silently falls back to the system font.
  • unloadAsync and unloadAllAsync always throw on iOS and Android. They are exported for API parity, but the native font loader has no unload method on either platform (only on web). This matches upstream exactly.
  • Keep the splash screen up while fonts load. Hold it with @symbiote-native/splash-screen until loaded is true to avoid a flash of unstyled text.
  • Web and server rendering are not ported. This repo has no web or SSR render target for any adapter, so isLoaded’s web fallback and the server pre-render pass are dropped.
  • expo-font’s config plugin is not ported. To bundle a static font into the native project, use the plain React Native route (android/app/src/main/assets/fonts/; on iOS, Xcode’s “Copy Bundle Resources” plus UIAppFonts in Info.plist), or call loadAsync with a local file:// URI at startup.

My custom font does not show; the text uses the system font. In order of likelihood: the fontFamily in the style is not the exact key you loaded; you rendered before loaded was true; the font file failed to load (check error, and getLoadedFonts() to see what is loaded).

How do I get bold or italic? Load each weight or style as its own family ('Inter-Bold': require('./Inter-Bold.ttf')) and set fontFamily: 'Inter-Bold'. Do not rely on fontWeight to pick a file for a custom family.

A font bundled into the native project works on Android but not iOS. For a font added through Xcode (UIAppFonts), iOS finds it by the font’s own name (its full or PostScript name), not the file name. That differs from runtime loading, where the name is whatever key you gave loadAsync.

Can I unload a font? No. unloadAsync throws on iOS and Android because the native font loader has no unload.

How do I avoid the flash of the wrong font at startup? Hold the splash screen until loaded is true (see the splash screen package), or return null until then.

Sources: expo/expo#33673 expo-font does not display custom fonts on Android, expo/expo#22074 custom font is not loading correctly on Android, DEV: why was my app not displaying custom fonts on iOS.

expo-font’s JS is hand-ported into this package’s core/, resolving ExpoFontLoader and ExpoFontUtils through expo-modules-core rather than the expo meta-package:

packages/font/src/
|-- core/ # font.ts, font-loader.ts, font-utils.ts, memory.ts (the loaded-fonts cache)
|-- react/ # hooks/ useFonts
|-- vue/ # composables/ useFonts
|-- svelte/ # runes/ useFonts
|-- solid/ # primitives/ createFonts
`-- angular/ # services/ FontsService

Each adapter file is lifecycle only. The native code is never vendored: expo-modules-autolinking resolves it from node_modules (see the native setup guide).