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 |
Installation
Section titled “Installation”npm install @symbiote-native/fontScaffolding 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>;}<script setup lang="ts">import { useFonts } from '@symbiote-native/font/vue';
const { loaded, error } = useFonts({ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),});</script>
<template> <text v-if="loaded" style="font-family: Inter-Regular">Hello</text></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { FontsService } from '@symbiote-native/font/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: `@if (fonts.loaded()) { <text style="font-family: Inter-Regular">Hello</text> }`,})export class App { readonly fonts = inject(FontsService).connect({ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'), });}connect() returns a pair of signals (loaded and error).
<script lang="ts"> import { useFonts } from '@symbiote-native/font/svelte';
const fonts = useFonts({ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'), });</script>
{#if fonts.loaded} <text style="font-family: Inter-Regular">Hello</text>{/if}import { createFonts } from '@symbiote-native/font/solid';
export function App() { const fonts = createFonts({ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'), });
return fonts.loaded() ? ( <text style={{ 'font-family': 'Inter-Regular' }}>Hello</text> ) : null;}Solid reserves use* for consuming existing state, so the primitive is createFonts. Call the
accessor (fonts.loaded()): a Solid component body runs once.
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') });Functions
Section titled “Functions”| 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 |
useFonts / createFonts / FontsService
Section titled “useFonts / createFonts / FontsService”| 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
fontFamilyin a style must match the map key exactly.'Inter-Regular'in the map andfontFamily: 'Inter'in the style silently falls back to the system font. unloadAsyncandunloadAllAsyncalways 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-screenuntilloadedis 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” plusUIAppFontsinInfo.plist), or callloadAsyncwith a localfile://URI at startup.
Common questions
Section titled “Common questions”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.
How the wrapper works
Section titled “How the wrapper works”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/ FontsServiceEach adapter file is lifecycle only. The native code is never vendored: expo-modules-autolinking
resolves it from node_modules (see the native setup guide).