Fonts

XAUI never loads a font file — it only names one. Expo loads it, the theme points at the loaded name, and a name that resolves to nothing falls back to the system face without saying so.

The theme names, Expo loads

fontFamilies is three names — the body face, the headings, and the monospace one. Every component reads them from the theme, so this is the only place a family is written.

// theme.ts
import { createTheme } from '@xaui/native/theme'
export const appTheme = createTheme({
fontFamilies: { body: 'Inter', heading: 'Inter', mono: 'JetBrains Mono' },
})

Those strings are looked up by the platform, not by XAUI. Loading the file is Expo's job, and there are two ways to do it.

Embed the file at build time

The option to prefer: the font ships inside the binary, so it is there on the first frame with nothing to await and no splash screen to hold.

// app.json
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": [
"./assets/fonts/Inter-Regular.ttf",
"./assets/fonts/Inter-Bold.ttf"
]
}
]
]
}
}

Run npx expo install expo-font, then npx expo prebuild --clean. This needs a development build — a config plugin does not apply in Expo Go.

Or load it at runtime

The way to stay in Expo Go. The font arrives after the first render, so hold the splash screen until it is ready.

import { useEffect } from 'react'
import { useFonts } from 'expo-font'
import * as SplashScreen from 'expo-splash-screen'
import { XAUIProvider } from '@xaui/native/theme'
import { appTheme } from './theme'
SplashScreen.preventAutoHideAsync()
export default function App() {
const [loaded] = useFonts({
'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
'Inter-Bold': require('./assets/fonts/Inter-Bold.ttf'),
})
useEffect(() => {
if (loaded) SplashScreen.hideAsync()
}, [loaded])
if (!loaded) return null
return (
<XAUIProvider theme={appTheme}>
<YourApp />
</XAUIProvider>
)
}

The if (!loaded) return null is not cosmetic: without it the first frame renders in the system font and every text jumps when the real one arrives.

Weights are not synthesised

On Android one family is one file is one weight: fontWeight will not reach Inter-Bold on its own, and the bold a component asks for silently stays regular. Two ways out.

  • Ship a variable font. One file whose weight axis answers the numeric weights, and fontWeight works everywhere.
  • Or give each weight its own family name and map the roles: { body: 'Inter-Regular', heading: 'Inter-Bold' }. The headings are then bold because they are a different family, not because of a weight.

The family name is not the file name

Android resolves the file name; iOS resolves the font's PostScript name. When the two differ, the same string works on one platform and falls back on the other.

Rename the file to match the PostScript name — Font Book shows it under the font's information — or branch on Platform.select in the theme.

It fails silently

A family name that resolves to nothing throws no error and logs nothing. The text renders in the system face, which is why a font that "did not apply" is almost always a name that never matched a loaded one. Check the name before the theme:

  • The file is listed in the config plugin, or in the useFonts map.
  • The development build was rebuilt after the plugin was added — npx expo prebuild --clean.
  • The name in fontFamilies is the one that was loaded, spelled the same way, PostScript name included on iOS.

The rest of the theme is on the theme page.