One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Fonts

Bundle fonts into the app at build time, or load them at runtime with One.UI.Fonts

There are two ways to get a custom font into a One app. Fonts you know at build time go in native.app.fonts, which copies the files into the iOS and Android projects so they are ready before the first frame. Fonts chosen at runtime, such as a user’s pick or a font served from your API, load with One.UI.Fonts.load or the useFonts hook.

Either way, you then use the font by name in a style: fontFamily: 'Inter-Bold'.

Usage

Bundle the files at build time:

import { one } from 'one/vite'
export default {
plugins: [
one({
native: {
app: {
name: 'MyApp',
fonts: ['./assets/fonts/Inter-Regular.ttf', './assets/fonts/Inter-Bold.ttf'],
},
},
}),
],
}

Or load at runtime and render once the font is ready:

import { useFonts } from 'one'
function App() {
const [loaded, error] = useFonts({ 'Inter-Bold': require('./assets/Inter-Bold.ttf') })
if (error) throw error
if (!loaded) return null
return <Home />
}

Key every font by its PostScript name, which is usually the file name without the extension. Each weight and style is its own file and its own name.

How it works

Build time. one prebuild copies each file in native.app.fonts next to the iOS app sources, adds it to the Xcode project’s Resources, and lists it under UIAppFonts in Info.plist, so iOS registers it at launch. On Android it copies the file to app/src/main/assets/fonts, where React Native finds fonts by file name. Changing the list needs a new prebuild and native build.

Runtime. Fonts.load is a Nitro module (OneFonts). Each entry resolves to a URI: a require() asset becomes the dev server URL in development and a bundled file in release builds; a string is used as given. The native side then works off the JS thread:

Fonts.isLoaded asks the platform synchronously. On iOS it checks UIFont by name and by family, so system fonts and fonts in UIAppFonts count too. On Android it checks fonts added at runtime and the files in assets/fonts.

useFonts reads isLoaded for every key on its first render, so a map that is already usable starts as loaded and never shows a blank frame. Otherwise it calls load once, on mount.

API reference

One.UI.FontsTypeDescription
load(fonts: FontMap) => Promise<void>Load every font in the map. Resolves when all are usable; rejects with the first failure.
(fonts: FontMap) => Promise<void>Load every font in the map. Resolves when all are usable; rejects with the first failure.
isLoaded(name: string) => booleanWhether the platform can draw this name now, including bundled and system fonts.
(name: string) => booleanWhether the platform can draw this name now, including bundled and system fonts.

FontMap maps a PostScript name to a source: a require() asset, or a URI string (file:// or https:// on native, any URL on web).

useFonts(fonts) returns [loaded, error]. error is the first rejection from load, or null.

On iOS and Android, a failed load rejects with an Error whose code says why:

CodeMeaning
E_FONTS_URINot a usable URI, an unsupported scheme, or a missing file
E_FONTS_DOWNLOADThe download failed or returned no bytes
E_FONTS_REGISTERThe platform rejected the file
E_FONTS_NAMEiOS only: the file registered, but not under your key

Platform differences

iOSAndroidWeb
Bundled fontsInfo.plist listassets/fonts folderCSS font-face
Runtime loadCore TextTypefaceFontFace
Wrong keyRejectsLoads anywayLoads under the key
Asset sourceYesYesURL strings only

Limits and gotchas

Examples

Load a font the user picks

import { One } from 'one'
async function applyFont(family: { name: string; url: string }) {
await One.UI.Fonts.load({ [family.name]: family.url })
return { fontFamily: family.name }
}

The first call downloads and caches the file; after that the font loads from disk.

Gate the first screen on fonts

import { useFonts } from 'one'
const fonts = {
'Inter-Regular': require('./assets/Inter-Regular.ttf'),
'Inter-Bold': require('./assets/Inter-Bold.ttf'),
}
export function FontGate({ children }: { children: React.ReactNode }) {
const [loaded, error] = useFonts(fonts)
if (error) throw error
return loaded ? children : null
}

Keeping fonts at module scope makes the map stable and reads clearly. If the same files are also listed in native.app.fonts, the gate is already open on the first render.

Edit this page on GitHub.