Providers
The seven built-in providers, and what each one can tell you.
A provider is a small adapter over one font source. createUnifont() takes them in order and asks each in turn until one answers.
import { createUnifont, providers } from 'unifont'
const unifont = await createUnifont([
providers.google(),
providers.bunny(),
providers.fontshare(),
providers.fontsource(),
providers.googleicons(),
providers.npm(),
])What each one can do
Not every provider implements every method. listFonts() is optional, for example.
| Provider | resolveFont | getFontProperties | listFonts | Needs options |
|---|---|---|---|---|
google | yes | yes | yes | no |
bunny | yes | yes | yes | no |
fontshare | yes | yes | yes | no |
fontsource | yes | yes | yes | no |
googleicons | yes | yes | yes | no |
npm | yes | yes | no | no |
adobe | yes | yes | no | id |
npm can't list, because the answer would be the whole registry. adobe can't, because Typekit ties everything to a project id.
providers.google()The largest open library. It serves woff2 with a unicode-range per subset, which is why one weight of a Google family often comes back as a dozen faces, one per script.
Two experimental options are worth knowing about. experimental.glyphs asks for a subset containing only the characters you name. That's how you get a 2 kB file for a logo:
providers.google({
experimental: {
glyphs: { Poppins: ['Hello', 'World'] },
},
})experimental.variableAxis gives you axes other than weight (slant, casual, cursive, mono) for families that publish them:
providers.google({
experimental: {
variableAxis: {
Recursive: { CASL: [['0', '1']], MONO: [['0', '1']] },
},
},
})You can pass both per family too, through resolveFont's options.google, which wins over whatever the provider was created with.
| Option | Type |
|---|---|
experimental.glyphs | { [family: string]: string[] } |
experimental.variableAxis | { [family: string]: Partial<Record<VariableAxis, ([string, string] | string)[]>> } |
options.google.experimental.glyphs | string[] |
options.google.experimental.variableAxis | Partial<Record<VariableAxis, ([string, string] | string)[]>> |
Bunny
providers.bunny()The same library as Google, mirrored on fonts.bunny.net without the analytics. The family names match, so you can swap bunny in wherever a request to Google would be a privacy problem. The compare view shows where the two disagree on subsets.
Fontshare
providers.fontshare()Indian Type Foundry's library. Around a hundred families, free for commercial use, and better drawn than most free fonts.
Fontshare doesn't split by subset, so a family comes back as one file per weight and style, with no unicode-range at all.
Fontsource
providers.fontsource()Open fonts packaged for npm and served from jsDelivr. Useful if you want the Google library without touching Google's servers, or the exact files a lockfile would pin.
Google Icons
providers.googleicons()Material Symbols, as variable icon fonts. experimental.glyphs matters more here than anywhere else. The full Material Symbols file is huge, and naming the icons you use cuts it to almost nothing.
providers.googleicons({
experimental: {
glyphs: {
'Material Symbols Outlined': ['arrow_right', 'favorite', 'arrow_drop_down'],
},
},
})As with Google, options.googleicons.experimental.glyphs narrows it to one family and wins. Either way it only works on the newer Material Symbols families.
| Option | Type |
|---|---|
experimental.glyphs | { [family: string]: string[] } |
options.googleicons.experimental.glyphs | string[] |
npm
providers.npm()Resolves fonts from npm packages, either from node_modules or from a CDN. It reads your package.json dependencies and recognises @fontsource/*, @fontsource-variable/*, and other font packages it knows about.
In a build tool you usually want local resolution with the CDN fallback turned off, so nothing leaves the machine:
import { access, readFile } from 'node:fs/promises'
providers.npm({
readFile: path => readFile(path, 'utf-8').catch(() => null),
exists: path => access(path).then(() => true).catch(() => false),
remote: false,
})With remote: false the sources come back as file:// URLs into node_modules, ready to copy or hash into your own build. Pass resolve for any layout where the package isn't linked under <root>/node_modules: pnpm's isolated store, hoisting to a monorepo root, Yarn PnP, or a bundler alias.
import { fileURLToPath } from 'node:url'
providers.npm({
readFile: path => readFile(path, 'utf-8').catch(() => null),
resolve: id => fileURLToPath(import.meta.resolve(id)),
})| Option | Type | Default |
|---|---|---|
cdn | string | 'https://cdn.jsdelivr.net/npm' |
remote | boolean | true |
readFile | (path: string) => Promise<string | null> | none |
exists | (path: string) => Promise<boolean> | falls back to readFile |
resolve | (id: string) => string | null | Promise<string | null> | import.meta.resolve, then <root>/node_modules/<id> |
root | string | '.' |
Return null from resolve (or throw) when a package isn't installed, and the provider falls back to the CDN unless remote is false. Without exists, checking for a font file means reading and decoding all of it through readFile.
Any CDN that serves packages by path works for cdn, so providers.npm({ cdn: 'https://esm.sh' }) is fine.
Three more options apply to one family at a time:
| Family option | Type | Default |
|---|---|---|
package | string | detected from package.json, or inferred from the family name |
version | string | 'latest', and only used for CDN resolution |
file | string | the per-weight and per-style entry points (<weight>.css, <weight>-italic.css), then index.css |
const { fonts } = await unifont.resolveFont('Roboto', {
options: {
npm: { package: '@fontsource/roboto', file: 'latin.css' },
},
})Adobe
if (!process.env.TYPEKIT_ID) {
throw new Error('TYPEKIT_ID is required to resolve Adobe fonts.')
}
providers.adobe({ id: process.env.TYPEKIT_ID })Adobe Fonts, through one Typekit project id or several. The id is yours, so this site can't ask Adobe on your behalf. Families that only exist on Adobe are missing from the catalogue here, even though unifont can resolve them on your own machine.
id is required and takes a string or a string[]. Read it from the environment rather than writing it into your source.
Limiting a single call
Providers belong to the instance, but you can limit any one call to a few of them:
const { fonts } = await unifont.resolveFont('Poppins', {}, ['bunny'])
const properties = await unifont.getFontProperties('Poppins', ['bunny'])That's how the compare view asks every provider the same question and labels each answer.