# expo-google-fonts: shipping Google fonts inside an Expo app without a CDN

> expo-google-fonts turns 1959 fonts and variants from fonts.google.com into per-family npm packages for Expo, with two loading strategies: a native embed through the expo-font config plugin, and a runtime useFonts hook that also works on web and in Expo Go. The choice between them decides whether your font is ready at launch or arrives after a native rebuild.

**expo/google-fonts** — Use any of the 1000+ fonts (and their variants) from fonts.google.com in your Expo app.

- Repository: https://github.com/expo/google-fonts
- Stars: 947 · Forks: 80
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/expo-google-fonts

## One npm package per font family

Adding a custom typeface to a mobile app usually means a small fight with build configuration, because a font file has to end up inside the native binary and the name you use in a stylesheet has to match what the platform registered. expo-google-fonts removes that fight by publishing a separate npm package for each family, so the Inter family arrives as @expo-google-fonts/inter with one directory per style inside it.

The scale is the point. The documentation states 1959 fonts and variants from fonts.google.com are available, and the topics of the repository are simply expo, font and fonts, which is consistent with a catalogue that is maintained rather than a hand written convenience wrapper. The licence field says MIT, and the README states the fonts are free to use and open source, and that the packages work across web, iOS and Android.

The packages are generated, not hand authored. The root package.json is a private workspace root named @expo/google-fonts at version 0.4.0, and its workspaces cover packages/* and font-packages/*. Alongside those directories sit font-archive/ and a GALLERY.md, which suggests a pipeline that pulls font files in, keeps an archive of them, and writes the publishable packages out of it. The scripts section shows the three commands that drive it: regenerate-all, sync-packages and generate-root-readme.

For a user of the packages, none of that machinery is visible. You install one package, import one style, and move on.

## Two loading paths that differ in more than API shape

The README splits the decision into a table, and the table is where the real trade-off sits. The expo-font config plugin embeds the font in the native build. The useFonts hook loads it from the JavaScript bundle at runtime. Both are supported, and the documentation is explicit that they differ in where the font file ends up and when the font becomes available.

The plugin path works on Android and iOS. It does not work on web, and it does not work in Expo Go, where it needs a development build. The hook path works on Android, iOS and web, and it is the only option in Expo Go.

The timing difference is the important one. With the config plugin the font is ready at app start, because the file was embedded at build time. With useFonts the font loads asynchronously, and the sample code returns null until the font is loaded, which means the app has to handle an empty first render.

The bundling difference follows from the timing difference. The plugin keeps the font file out of the JavaScript bundle entirely. The hook puts the font file in the bundle, one file per style that you import. And the last row of the table records the operational cost: adding or removing a style with the plugin requires a new native build, while with the hook it does not.

So the choice is between a font that is guaranteed to be there at launch and a font that can be added without touching the native toolchain.

## The config plugin points at a file path, not a font name

Configuring the plugin means giving it the path to a font file inside the package. Each style lives in its own directory, and the file name encodes the weight, so the Inter 900 Black style is a .ttf file under a 900Black directory:

```json
{
  "expo": {
    "plugins": [
      [
        "expo-font",
        {
          "fonts": ["node_modules/@expo-google-fonts/inter/900Black/Inter_900Black.ttf"]
        }
      ]
    ]
  }
}
```

The entry has to be added once per style, and the README is clear that a new native build is needed each time a style is added or removed. There is also a path assumption to watch: the plugin path starts at node_modules, so a project that installs dependencies somewhere other than a node_modules directory at the project root has to adjust the path to match its own setup.

The naming trap appears immediately afterwards. On Android the font family name is the file name, and on iOS it is the PostScript name of the font, which is usually different. The same style is therefore referenced with two different strings, and the documentation shows the pattern for selecting between them:

```js
import { Platform, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text
        style={{
          fontFamily: Platform.select({ android: 'Inter_900Black', ios: 'Inter-Black' }),
          fontSize: 40,
        }}>
        Inter Black
      </Text>
    </View>
  );
}
```

A stylesheet that hardcodes one name will render on one platform and fall back to the system font on the other, and the fallback is silent.

## Metro keeps every style you import from the package root

The hook path is simpler to write and has one documented failure mode. The README gives a warning about importing from the package root, for example import { Inter_900Black } from '@expo-google-fonts/inter'. That import works, and it is exactly what you should not do.

The reason is Metro. The root module requires every style in the family, and Metro does not remove the styles that you do not use, so the unused font files stay in your app bundle. For a family with many styles, or for several families at once, the documentation says this can add megabytes to the app. The fix is to import each style from its own subpath, because each subpath contains one font file, so only the styles you import are included.

The working version of the hook looks like this:

```js
import React, { useState, useEffect } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { useFonts } from '@expo-google-fonts/inter/useFonts';
import { Inter_900Black } from '@expo-google-fonts/inter/900Black';

export default function App() {
  let [fontsLoaded] = useFonts({
    Inter_900Black,
  });

  if (!fontsLoaded) {
    return null;
  }

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text style={{ fontFamily: 'Inter_900Black', fontSize: 40 }}>Inter Black</Text>
    </View>
  );
}
```

One thing this buys you is consistency: the name you give the style in useFonts becomes the fontFamily value, and the documentation states it is the same on every platform. The Platform.select pattern from the plugin path is not needed here, which removes a whole class of per platform bugs.

The trade is the opposite one. The hook path never needs a native rebuild, but it needs a loading state, and the font is not painted until it arrives.

## Hiding the empty first render behind the splash screen

The null return in the hook example is the honest part of the design, and it is also the part that will be visible to users if you leave it there. The README points at the standard remedy: the expo-splash-screen package, which lets you keep the splash screen visible while the fonts load and then hide it once the app has rendered with some initial content. The documentation links a minimal example in the Expo fonts documentation for the exact pattern.

That is a practical answer to the loading problem the hook path introduces. The app boots, the splash screen stays up, the hook resolves, and the first real frame already has the right font. Without the splash screen package, the alternative is to render a fallback typography briefly, which on a marketing or onboarding screen is quite visible.

The plugin path needs none of this. Because the font was embedded at build time, it is ready when the app starts, and the documentation says plainly that no loading code is needed. The trade is the rebuild cycle.

So the real decision is not API ergonomics, it is your release process. If you already run EAS Build or development builds for every release, a rebuild per new style is a normal part of your pipeline and the plugin path gives you a font at frame zero. If your team iterates in Expo Go and only occasionally builds, the hook path keeps the loop tight, at the price of a loading state to handle.

## A generated monorepo rather than a hand maintained catalogue

The repository layout explains how 1959 fonts and variants stay in sync with the source catalogue. The root package.json is a private workspace root with two workspace globs, packages/* and font-packages/*, and a font-archive/ directory sits alongside them. The scripts give the pipeline names: regenerate-all, sync-packages and generate-root-readme, each delegating to a generator workspace.

The README itself carries a matching note in a comment at the top, saying the file is generated by @expo-google-fonts/generator and that anyone who wants to modify it should go to packages/generator/templates rather than editing the output. That is the clearest statement of the architecture in the whole repository: the README and the packages are build artefacts of the generator, and the templates are the source of truth.

Two consequences follow for someone depending on the packages. First, the package contents are consistent across families, because one template produces all of them, which is why the per-style subpath convention and the file naming are reliable enough to build a plugin path from. Second, upstream changes to fonts.google.com arrive as a regeneration rather than as a hand edit, so the upgrade path is an npm version bump rather than a code review of 1959 packages.

The GALLERY.md file at the root, and the gifs/ directory referenced by the title image, are part of the same generated surface. They exist to present the catalogue, not to serve the runtime.

## MIT licensing, no tagged releases, recent pushes

The licence field on the repository says MIT, and the README states that the fonts and the packages are free to use and open source. For a font catalogue that ships typefaces with their own upstream licensing, that is worth stating plainly: the packages are MIT licensed and the README says the fonts are free to use and open source, so a project embedding them does not inherit a copyleft obligation from this repository. That is a licensing observation rather than legal advice, and the individual font licences on fonts.google.com remain the upstream reference for a specific family.

On maintenance, the repository is not archived and the last push was on 2026-10-12. The project publishes no GitHub releases, so there is no changelog to read and no tagged version to pin; the versioning that matters to a consumer lives in the npm package versions, where the root workspace sits at 0.4.0 and each family is published separately under the @expo-google-fonts scope. A consumer therefore pins a version of one small package rather than a version of the whole catalogue.

One thing the documentation does not cover is what happens when a family on fonts.google.com is renamed or a style is re-cut upstream. The regeneration pipeline handles catalogue changes, but the README says nothing about deprecation notices or about how a consumer is told that a path like node_modules/@expo-google-fonts/inter/900Black/Inter_900Black.ttf has moved. Anyone depending on a specific file path in a plugin config should expect to touch that path when the catalogue shifts.

## Bundling font files yourself instead of using a per-family package

The alternative to this project is not another font library but the manual route: take the font files from fonts.google.com yourself, place them in your own assets, and point expo-font at those paths, or serve them from your own web host. The difference in approach is straightforward.

The per-family package gives you a versioned artefact on npm, a stable directory convention, and a file set that matches the catalogue. The manual route gives you control over exactly which files land in the bundle, which is a real advantage if you use one weight of one family and do not want the shape of a whole family package in your dependency tree. It also costs you the update path, since nobody regenerates your assets for you when the upstream family changes.

A second alternative worth naming is a runtime font download from your own server, which is how a web-first design would handle typography. That approach is not what these packages do. Everything here is bundled at build time or shipped inside the JavaScript bundle, which is what makes the fonts work offline and what makes the plugin path able to guarantee a font at the first frame. The cost of that guarantee is the megabytes and the rebuild cycle described earlier.

Pick the package route if you want the catalogue and the convention. Pick the manual route if the bundle size of a single unused weight matters more than having the update path.

## Conclusion

expo-google-fonts is a good fit for an Expo project that wants a specific Google family on device without a runtime font download, especially where the app already ships through EAS Build or development builds and can absorb a native rebuild per style. It is the wrong choice for a project that only ever targets Expo Go, or one that needs a font to render on the very first frame of a cold start without handling a loading state. Verify two things before committing: the per-style import path, because importing from the package root pulls every style in the family into the bundle, and the platform specific fontFamily name, since Android uses the file name while iOS uses the PostScript name.

## FAQ

### How do I install a Google font in an Expo project with expo-google-fonts?

The README gives the install command as npx expo install @expo-google-fonts/inter expo-font, where the package name follows the family you want. After that you choose either the expo-font config plugin or the useFonts hook to load it.

### What is the difference between the expo-font config plugin and the useFonts hook?

The config plugin embeds the font in the native build so it is ready at app start and needs a new native build for every style added, while useFonts loads the font from the JavaScript bundle at runtime, works on web and in Expo Go, and requires handling a loading state. The README presents both as supported and compares them in a table.

### Why does my fontFamily work on Android but not on iOS?

On Android the font family name is the file name, while on iOS it is the font's PostScript name, which is usually different. The documentation shows selecting the name per platform with Platform.select, and the useFonts hook avoids the problem because the style name is the same on every platform.

### How many fonts are available in expo-google-fonts?

The documentation states 1959 fonts and variants from fonts.google.com, published as per-family packages under the @expo-google-fonts npm scope. The repository ships a GALLERY.md and a font-archive/ directory alongside the generated packages.

### Can I use expo-google-fonts in Expo Go?

Yes, through the useFonts hook, which the README describes as the only option in Expo Go. The expo-font config plugin does not run in Expo Go and requires a development build there. The README also suggests the expo-splash-screen package to cover the loading period.

### Does importing from the package root bloat my app bundle?

It can. The README warns that the root module requires every style in the family and that Metro does not remove the styles you do not use, so unused font files stay in the bundle, which for a family with many styles can add megabytes. Importing from the per-style subpath keeps only the styles you import.

## Sources

- [expo/google-fonts on GitHub](https://github.com/expo/google-fonts)
- [Issues](https://github.com/expo/google-fonts/issues)
- [License: MIT](https://github.com/expo/google-fonts/blob/main/LICENSE)
- [README](https://github.com/expo/google-fonts/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/expo-google-fonts
