# unifont, in full > Every prose page on unifont.dev, concatenated. The machine-readable index is at https://unifont.dev/llms.txt and the API description at https://unifont.dev/openapi.json. # unifont: every font CDN, one lookup > unifont reads font metadata from Google Fonts, Bunny Fonts, Fontshare, Fontsource, Google Icons and npm through one interface. `unifont` is an open-source JavaScript library for reading font metadata from font CDNs: weights, styles, subsets, unicode ranges and `@font-face` data, in the same shape for every provider. It runs in Node, Bun, Deno, workers and the browser, and it is what [`@nuxt/fonts`](https://fonts.nuxt.com) and [`fontless`](https://github.com/unjs/fontaine/tree/main/packages/fontless) use underneath. unifont.dev is the project site: a catalogue of every family the providers will list, a specimen and metadata page per family, a provider comparison, the documentation, and a public HTTP API that every page here is built on. ## Install ```bash npm install unifont ``` ```ts import { createUnifont, providers } from 'unifont' const unifont = await createUnifont([ providers.google(), providers.fontshare(), ]) await unifont.getFontProperties('Switzer') ``` ## Providers Google Fonts, Bunny Fonts, Fontshare, Fontsource, Google Icons, npm, and Adobe Fonts (with a Typekit project id). ## Where to go next - [Catalogue](/fonts): every family the providers will list, searchable and filterable by provider. - [Compare](/compare): the same family asked of every provider, side by side. - [Documentation](/docs): install it, resolve a family, cache the results, write your own provider. - [HTTP API](/api): the public endpoints behind every page, plus an MCP server at `/mcp`. - [About](/about), [Contact](/contact), [Privacy](/privacy). - [Machine-readable index](/llms.txt), [OpenAPI specification](/openapi.json), [sitemap](/sitemap.xml). --- # Font catalogue > Every font family unifont can list, across Google Fonts, Bunny Fonts, Fontshare, Fontsource and Google Icons. Search every family the providers will list. Each family has a page with a specimen, the weights, styles and subsets the provider publishes, unicode coverage, transfer size, and `@font-face` CSS. The catalogue is the union of the provider indexes, so a family published by more than one provider appears once with every provider that carries it. Google Fonts, Bunny Fonts, Fontshare, Fontsource and Google Icons can all be listed; npm is the whole registry and Adobe Fonts needs a per-user Typekit id, so neither can be enumerated here. Search is ranked exact, then prefix, then word, then substring, and never fuzzy: a misspelt family returns nothing rather than the wrong answer. A family page lives at `/fonts/`. The same data programmatically: ```bash # search curl "https://unifont.dev/api/v1/fonts?q=grotesk&limit=5" # one family curl "https://unifont.dev/api/v1/fonts/Newsreader?weights=400,600&subsets=latin" ``` See the [HTTP API reference](/api) or the [OpenAPI specification](/openapi.json). --- # Compare font providers > Ask every font provider for the same family and compare weights, subsets, hosts, file counts and transfer size. Providers disagree: the same family can publish different weights, different subsets, and different file counts depending on who serves it. This page asks every provider that needs no credentials for one family and lays the answers side by side. What differs in practice: which weights exist (a provider may publish a variable axis where another ships static cuts), which subsets are available, how many files a selection resolves to, the host the files come from, the transfer size, and the fallback stack the provider suggests. A provider that does not publish the family at all is reported as unavailable rather than omitted. ```bash curl https://unifont.dev/api/v1/fonts/Inter/compare ``` See the [HTTP API reference](/api) or the [OpenAPI specification](/openapi.json). --- # unifont.dev HTTP API > Every page on unifont.dev is also an HTTP endpoint: search, metadata, CSS, provider comparison and unicode coverage. Every page on unifont.dev is a thin layer over these endpoints. They are public, they need no authentication, they are cached, and they will not change while they are under `/v1`. Machine-readable: [`/openapi.json`](/openapi.json). Not for production use: this API is best-effort and may be rate-limited or withdrawn. If a deployment of yours depends on it, run `unifont` yourself. ## GET /api/v1/providers `listProviders`: Every provider unifont supports, with the number of families each one lists and whether it answered while the current index was built. - `200` Every provider, in cascade order. ## GET /api/v1/status `getStatus`: How the merged index is doing right now: family counts, which providers answered, and how old the index is. - `200` Current index state. ## GET /api/v1/contributors `listContributors`: Everyone who has landed a commit in `unjs/unifont`, ranked by commit count. - `200` Contributors, most commits first. ## GET /api/v1/fonts `searchFonts`: Search the merged catalogue of families. Ranked exact, then prefix, then word, then substring. Never fuzzy, so a typo returns nothing rather than the wrong family. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `q` | query | string | no | What to search for. Omit to list everything. | | `provider` | query | string | no | Limit results to one provider. `adobe` cannot list its families and is rejected. | | `variable` | query | boolean | no | Keep only families publishing a weight range. A family whose provider publishes no properties is dropped by any filter. | | `italic` | query | boolean | no | Keep only families publishing an italic. | | `subset` | query | string | no | Keep only families covering this script, as `/api/v1/facets` names them. | | `limit` | query | integer | no | How many families to return. | | `offset` | query | integer | no | How many matches to skip. | - `200` Matching families. - `400` Unknown provider, or a provider that cannot list its families. ## GET /api/v1/fonts/{family} `getFont`: Everything a provider knows about a family, and the faces a selection resolves to, including `@font-face` CSS for it. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | | `provider` | query | string | no | Limit the provider cascade to these providers (comma-separated). | | `weights` | query | string | no | Weights to resolve (comma-separated). A range such as `200 800` resolves a variable axis. | | `styles` | query | string | no | Styles to resolve (comma-separated): `normal`, `italic` or `oblique`. | | `subsets` | query | string | no | Unicode subsets to resolve (comma-separated), e.g. `latin`, `latin-ext`, `cyrillic`. | | `formats` | query | string | no | Font formats to resolve (comma-separated). Defaults to `woff2`. | - `200` The resolved family. - `400` No family was given. - `404` No provider publishes the family. It may still exist on Adobe Fonts, which cannot be listed. ## GET /api/v1/fonts/{family}/css `getFontCss`: `@font-face` CSS for one family, to read, paste or fetch from a script. A selection that resolves to nothing returns an empty stylesheet with a comment, not an error. Linking it as a stylesheet from another site is refused: resolve the family with `unifont` and serve the CSS yourself. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | | `provider` | query | string | no | Limit the provider cascade to these providers (comma-separated). | | `weights` | query | string | no | Weights to resolve (comma-separated). A range such as `200 800` resolves a variable axis. | | `styles` | query | string | no | Styles to resolve (comma-separated): `normal`, `italic` or `oblique`. | | `subsets` | query | string | no | Unicode subsets to resolve (comma-separated), e.g. `latin`, `latin-ext`, `cyrillic`. | | `as` | query | string | no | Rename the family in the output, so several providers can be loaded side by side. | - `200` The stylesheet. - `400` No family was given. ## GET /api/v1/css `getCatalogueCss`: One stylesheet for up to 40 families, so a grid of specimens costs a single request. A family that cannot be resolved becomes a comment rather than breaking the sheet. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `families` | query | string | yes | Families to resolve (comma-separated, up to 40). | | `weights` | query | string | no | Weights to resolve (comma-separated). Defaults to the variable range, or the weight nearest 400. | | `subsets` | query | string | no | Subsets to resolve (comma-separated). Defaults to `latin` where the family publishes it. | | `preset` | query | string | no | `specimen` cuts each face to the characters this site’s grids set, which only Google honours. Omit it to receive faces that can set any text. | - `200` The stylesheet. - `400` `families` was missing or empty. ## GET /api/v1/facets `listFacets`: Counts for each filter the catalogue offers, so a caller knows what a filter would keep before applying it. - `200` Filter counts. ## GET /api/v1/transfer `getBaselineTransferSizes`: Bytes on the wire for up to 40 families, all quoted against one selection (400 and 700, latin, woff2) so that two families are compared on the same terms. Measured with HEAD requests, so nothing is downloaded. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `families` | query | string | yes | Families to measure (comma-separated, up to 40). | - `200` Measured transfer sizes. - `400` `families` was missing or empty. ## GET /api/v1/fonts/{family}/fallback `getFontFallback`: A `@font-face` that makes a generic match this family’s metrics, so a page does not reflow when the real font lands. Metrics are read from the served file, and the face points at families a system may actually have rather than at the generic itself, which resolves nowhere. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | | `provider` | query | string | no | Limit the cascade to these providers (comma-separated). | - `200` The generated face and its descriptors. - `400` No family was given. - `404` No provider could resolve this family. ## GET /api/v1/fonts/{family}/budget `getTextBudget`: Prices a string three ways against the same family: every file the selection publishes, only the files the text pulls through `unicode-range`, and, for a provider that subsets to a glyph list, the subset. For a family split into a hundred subset files the three differ by orders of magnitude. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | | `text` | query | string | yes | The string to price. Whitespace is collapsed, and it is capped at 64 characters. | | `weights` | query | string | no | Weights to price (comma-separated). Default 400. | | `styles` | query | string | no | Styles to price (comma-separated). Default normal. | | `provider` | query | string | no | Limit the cascade to these providers (comma-separated). | - `200` The three plans, measured the same way. - `400` No family, or no `text` to price. - `404` No provider knows this family, or none could resolve the selection. ## GET /api/v1/fonts/{family}/compare `compareFontProviders`: Ask every provider that needs no credentials for the same family, and see how the answers differ: weights, styles, subsets, file counts and transfer size. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | - `200` One row per provider. - `400` No family was given. ## GET /api/v1/fonts/{family}/coverage `getFontCoverage`: Which characters the resolved faces can actually draw, based on their `unicode-range`. Checks a set of built-in samples, or your own string. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | | `provider` | query | string | no | Limit the provider cascade to these providers (comma-separated). | | `text` | query | string | no | Check this text instead of the built-in samples. | - `200` Coverage per sample. - `400` No family was given. - `404` No provider publishes the family. ## GET /api/v1/fonts/{family}/transfer `getFontTransferSize`: How much a selection weighs, measured with `HEAD` requests against the provider CDN. No font file is downloaded. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | | `provider` | query | string | no | Limit the provider cascade to these providers (comma-separated). | | `weights` | query | string | no | Weights to resolve (comma-separated). A range such as `200 800` resolves a variable axis. | | `styles` | query | string | no | Styles to resolve (comma-separated): `normal`, `italic` or `oblique`. | | `subsets` | query | string | no | Unicode subsets to resolve (comma-separated), e.g. `latin`, `latin-ext`, `cyrillic`. | - `200` Measured transfer size. - `400` No family was given. - `404` No provider publishes the family. ## GET /api/v1/fonts/{family}/stacks `listStacksUsingFamily`: Every published stack with this family in any role, found by asking a backlink index which records point at the family’s canonical page. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `family` | path | string | yes | Font family name, percent-encoded. Matching ignores case. | - `200` Stacks using the family. - `400` No family was given. ## GET /api/v1/stacks `listStacks`: Stacks anyone has published as `dev.unifont.stack` records, newest first. Found through the constellation backlink index, then read from the account that owns each one. Nothing is stored here. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Up to 100. Default 24. | - `200` Recently published stacks. ## GET /api/v1/stacks/{handle} `listStacksByAuthor`: Read straight from that account, so it is complete whether or not any index knows about them. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `handle` | path | string | yes | A handle or a DID. | - `200` Their stacks. - `404` No account answers to that handle. ## GET /api/v1/stacks/{handle}/{rkey} `getStack`: One published stack, read from the account that owns it rather than from any index. | Parameter | In | Type | Required | Notes | | --- | --- | --- | --- | --- | | `handle` | path | string | yes | A handle or a DID. | | `rkey` | path | string | yes | The record key. | - `200` The stack, with its author. - `404` No such record. ## POST /mcp `callMcp`: MCP server over Streamable HTTP. Tools: `search_fonts`, `get_font`, `get_font_css`, `compare_providers`, `check_coverage`, `list_providers`. Add it with `claude mcp add --transport http unifont https://unifont.dev/mcp`. - `200` A JSON-RPC 2.0 response. A notification is answered with `202` and no body. - `202` A notification was accepted. - `400` The body was not a JSON-RPC 2.0 request. ## Conventions - **Family names** are percent-encoded: `/api/v1/fonts/Big%20Shoulders%20Display`. Matching ignores case. - **Unknown families** return `404` with a message naming the family. - **Partial answers** name the providers that failed in `unavailable`, rather than quietly returning a short list. - **Caching**: metadata for an hour, CSS for a day, both with `stale-while-revalidate`. Font files come from the provider CDN; this API never proxies them. --- # Introduction > What unifont does, what it doesn't, and why. No two font CDNs are accessed in the same way. Google returns a stylesheet you have to parse. Fontshare returns JSON, with its own names for weights. Fontsource publishes to npm. And Adobe needs custom configuration and a project id. `unifont` is a single layer for all font providers. Call it with a family and you'll get back a consistent response with weights, styles, subsets, unicode ranges and `@font-face` data. ```ts import { createUnifont, providers } from 'unifont' const unifont = await createUnifont([ providers.google(), providers.fontshare(), ]) const { fonts, fallbacks } = await unifont.resolveFont('Switzer') ``` ## What it doesn't do `unifont` reads metadata. It doesn't download fonts, subset them, generate CSS, or cache anything for you. You get the data back and decide what to do with it. That's deliberate. Check out [`fontless`](https://github.com/unjs/fontaine/tree/main/packages/fontless) for zero-config integration for Vite, or the [`@nuxt/fonts`](https://fonts.nuxt.com), the equivalent for Nuxt. Or build your own integration: [Reducing layout shift](/docs/layout-shift) shows both paths. ## Where it runs Node, Bun, Deno, workers, and the browser. In a browser or a web container, CORS blocks the provider APIs, so requests go through a proxy instead. See [Browser and web containers](/docs/browser). ## This site Every page here is also an endpoint: the [HTTP API](/api) is described by [`/openapi.json`](/openapi.json), there is an MCP server at `/mcp`, [`/llms.txt`](/llms.txt) indexes the site for agents, and any prose page will answer in markdown if you ask for `text/markdown` or append `.md` to its path. Everything on unifont.dev comes from `unifont`. The catalogue is `listFonts()`, the family pages are `getFontProperties()` and `resolveFont()`, and the type you're reading is set with a stylesheet built from resolved metadata. --- # Getting started > Install unifont, resolve a family, and turn the answer into CSS. `unifont` is ESM-only, and has no peer dependencies. ```bash npm i unifont ``` ## Create an instance `createUnifont()` takes an array of providers. Order matters. They're tried in turn, and the first one that knows the family wins. ```ts import { createUnifont, providers } from 'unifont' const unifont = await createUnifont([ providers.google(), providers.fontshare(), providers.fontsource(), ]) ``` ## Ask what a family has `getFontProperties()` tells you what a family publishes before you resolve anything. It returns `undefined` if no provider recognises the name. ```ts const properties = await unifont.getFontProperties('Newsreader') properties?.weights // ['200 800'], a variable range properties?.styles // ['normal', 'italic'] properties?.subsets // ['cyrillic', 'greek', 'latin', 'latin-ext', 'vietnamese'] properties?.axes // [{ tag: 'opsz', min: 6, max: 72, defaultValue: 14 }, …] properties?.provider // 'google' ``` A missing field means the provider doesn't publish that information. Variable families report a range as one `' '` string, rather than listing every weight. ## Resolve the faces ```ts const { fonts, fallbacks, provider } = await unifont.resolveFont('Newsreader', { weights: ['400', '600'], styles: ['normal'], subsets: ['latin'], }) ``` `fonts` is an array of `@font-face` descriptors: `src`, `weight`, `style`, `unicodeRange`, and a `meta.subset` label if the provider gave one. `fallbacks` holds the generic family the provider suggests (`serif`, `sans-serif`). (That's what you need if you're generating metric-adjusted fallbacks; see [Reducing layout shift](/docs/layout-shift).) ## Turn it into CSS `unifont` stops at the data. Writing it out takes a few lines, and doing it yourself means the CSS comes out the way you want it: ```ts const css = fonts.map((face) => { const src = face.src .map(source => 'name' in source ? `local("${source.name}")` : `url("${source.url}")${source.format ? ` format("${source.format}")` : ''}`) .join(', ') return [ '@font-face {', ` font-family: "Newsreader";`, ` src: ${src};`, face.weight ? ` font-weight: ${Array.isArray(face.weight) ? face.weight.join(' ') : face.weight};` : '', ` font-style: ${face.style ?? 'normal'};`, ` font-display: ${face.display ?? 'swap'};`, face.unicodeRange ? ` unicode-range: ${face.unicodeRange.join(', ')};` : '', '}', ].filter(Boolean).join('\n') }).join('\n\n') ``` Or don't bother. Every family page on this site serves the same CSS at `/api/v1/fonts//css`, and the [API reference](/api) lists the parameters. ## Errors If a provider fails, `unifont` logs it and carries on with the next provider in the list, so one dead API doesn't break your build. A provider is only started the first time a request reaches it, so an outage at a provider that earlier ones already serve costs nothing. Pass `throwOnError` if you'd rather it stopped: ```ts const unifont = await createUnifont([providers.google()], { throwOnError: true }) ``` Even then, an error is only raised if no provider could serve the family. If one provider fails and a later one succeeds, the failure is a warning. When it does throw with more than one failure, you get an `AggregateError` whose `errors` carry the individual causes. --- # 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. ```ts 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. ## Google ```ts 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: ```ts providers.google({ experimental: { glyphs: { Poppins: ['Hello', 'World'] }, }, }) ``` If the characters you name span a whole curated subset, that subset's file will be served as is, since Google optimises those better than the files it builds on demand. Every other character you name, including any outside the subsets you asked for, comes back in one face built for exactly those characters. `experimental.variableAxis` gives you axes other than weight (slant, casual, cursive, mono) for families that publish them: ```ts 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> }` | | `options.google.experimental.glyphs` | `string[]` | | `options.google.experimental.variableAxis` | `Partial>` | ## Bunny ```ts 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](/compare) shows where the two disagree on subsets. ## Fontshare ```ts 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 ```ts 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 ```ts 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. ```ts 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 ```ts 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: ```ts 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. Font files and `@import` rules that resolve outside the package directory are skipped. Pass `resolve` for any layout where the package isn't linked under `/node_modules`: pnpm's isolated store, hoisting to a monorepo root, Yarn PnP, or a bundler alias. ```ts 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` | none | | `exists` | `(path: string) => Promise` | falls back to `readFile` | | `resolve` | `(id: string) => string \| null \| Promise` | `import.meta.resolve`, then `/node_modules/` | | `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. The family name has to match what the package's own `@font-face` rules declare, which is not always the name you would search a CDN for. The [catalogue](/fonts?provider=npm) lists families this provider can resolve that the font CDNs do not carry, each with the exact call it needs. 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 (`.css`, `-italic.css`), then `index.css` | ```ts const { fonts } = await unifont.resolveFont('Roboto', { options: { npm: { package: '@fontsource/roboto', file: 'latin.css' }, }, }) ``` ## Adobe ```ts 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: ```ts const { fonts } = await unifont.resolveFont('Poppins', {}, ['bunny']) const properties = await unifont.getFontProperties('Poppins', ['bunny']) ``` That's how the [compare view](/compare) asks every provider the same question and labels each answer. --- # Resolving fonts > Weights, styles, subsets and formats, and the bit that catches people out. `resolveFont()` takes a family, some options, and optionally a list of providers to try. ```ts const { fonts, fallbacks, provider } = await unifont.resolveFont('Poppins', { weights: ['400', '700'], styles: ['normal', 'italic'], subsets: ['latin', 'latin-ext'], formats: ['woff2'], }) ``` Every option is a hint, not a guarantee you'll get back exactly that. For example, if you ask for `otf` from a provider that only serves woff2, you'll get woff2. ## Weights Default: `['400']`. Discrete weights are strings. A variable range is one string with a space: `'100 900'`. ```ts await unifont.resolveFont('Poppins', { weights: ['300', '500 900'] }) ``` If no variable font covers the range you asked for, `unifont` won't expand that range into every published weight. It picks a few: the weights at each end, plus whichever is nearest `400`, so body text still looks right. `'500 900'` on a static family gives you two or three faces, not five. If no published weight falls inside the range at all (`'450 480'` on a family that ships 400 and 500) you get the nearest weight rather than nothing. List the weights you want if you need the ones in between: ```ts await unifont.resolveFont('Poppins', { weights: ['500', '600', '700', '800', '900'] }) ``` ## Styles Default: `['normal', 'italic']`. ```ts await unifont.resolveFont('Poppins', { styles: ['normal'] }) ``` Worth narrowing. The default doubles the number of files, for a page that may never use italic. ## Subsets Default: `['cyrillic-ext', 'cyrillic', 'greek-ext', 'greek', 'vietnamese', 'latin-ext', 'latin']`. ```ts await unifont.resolveFont('Poppins', { subsets: ['latin'] }) ``` Subsets make the biggest difference to how much you send, and they're the easiest to get wrong. Drop `latin-ext` and you lose characters that Polish, Czech and Turkish need. Every family page on this site shows a [coverage check](/fonts/Poppins) against sample text, so you can see what narrower subsets cost. Providers that don't split by subset, like Fontshare and most of npm, ignore this option and return whole files with no `unicode-range`. ## Formats Default: `['woff2']`. ```ts await unifont.resolveFont('Poppins', { formats: ['woff2', 'woff'] }) ``` Every browser has supported woff2 for years. Only ask for more if you know why you need it. ## Variable axes Weight and style aside, a variable family can publish any number of axes: optical size (`opsz`), width (`wdth`), slant (`slnt`), and custom ones like Recursive's `CASL` and `MONO`. `variableAxis` asks for values along them, keyed by axis tag: ```ts await unifont.resolveFont('Recursive', { weights: ['300 1000'], variableAxis: { CASL: [1], slnt: [{ min: -15, max: 0 }], }, }) ``` A number is one value, and `{ min, max }` is an inclusive range, which you can also write as a `[min, max]` pair. Strings are accepted everywhere a number is. `getFontProperties()` reports the axes a family publishes, so you can check what's available before asking: ```ts const properties = await unifont.getFontProperties('Recursive') properties?.axes // [{ tag: 'CASL', min: 0, max: 1, defaultValue: 0 }, …] ``` What that buys you depends on the provider, and on the family. Google instances the font file itself, so the file you get back contains only the axes and values you asked for, and out-of-range values are clamped to what the family publishes. An axis a family doesn't have is dropped. Other providers serve the whole variable file, so `unifont` sets `font-variation-settings` on the resolved faces instead: the file is the same size, but the text renders at the axis values you asked for. A face that already carries a descriptor keeps the tags you didn't ask about. The fallback applies an axis only when you name a single value for it. A range, or several values for one axis, needs either a provider that can instance the file or a subsetting step of your own. The result records what happened to each axis you asked for, so a tool that processes font files can act on what's left: ```ts const { variableAxis } = await unifont.resolveFont('Recursive', { variableAxis: { CASL: [1], slnt: [{ min: -15, max: 0 }] }, }) variableAxis // { // CASL: { values: ['1'], appliedAs: 'variation-settings' }, // slnt: { values: [['-15', '0']], appliedAs: 'none' }, // } ``` `appliedAs` is `font-file` when the file itself is instanced or limited to those values, `variation-settings` when they're in the `font-variation-settings` descriptor, and `none` when they're left for you, either because the family doesn't publish the axis or because a single font face can't express what you asked for. It's reported per axis, so one request can come back with all three. Values come back normalised: a single value as a string, a range as a `[min, max]` pair. The whole field is `undefined` when you didn't ask for any axes. `unifont` writes `font-variation-settings` as `"" `, comma separated, so a consumer that instances an `appliedAs: 'variation-settings'` axis itself can drop that tag from the descriptor. ## Per-family provider options Some providers accept options for a single family. They're grouped by provider name, and typed from the providers you passed to `createUnifont()`: ```ts const { fonts } = await unifont.resolveFont('Poppins', { options: { google: { experimental: { glyphs: ['Hello', 'World'] }, }, }, }) ``` ## What comes back ```ts interface FontFaceData { src: Array<{ name: string } | { url: string, format?: string, tech?: string }> display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional' weight?: string | number | [number, number] stretch?: string style?: string unicodeRange?: string[] featureSettings?: string variationSettings?: string meta?: { priority?: number, subset?: string, init?: RequestInit } } ``` A `src` entry with `name` is a local font. One with `url` is remote. `meta.subset` is the provider's own name for the range. `meta.init` carries a `RequestInit` if the provider needs headers to fetch the file. Pass it through if you're downloading the font rather than linking to it. --- # Reducing layout shift > Use font metrics to build a fallback that takes up the same space as the real font. In most cases, a webfont arrives after the first paint, so text is first set in whatever comes next in the stack. If that family is a different size, the page reflows when the real font lands. [`fontaine`](https://github.com/unjs/fontaine) solves that issue: it generates a `@font-face` rule that points at a locally installed font and scales it with `size-adjust`, `ascent-override`, `descent-override` and `line-gap-override` until it occupies the same space as the real one. `unifont` and `fontaine` are designed to work together. `unifont` resolves the location of the font and its descriptors; `fontaine` turns those into the fallback rule. ## Let an integration do it If you want fonts to work rather than to wire this up yourself, use an integration. Each of these resolves with `unifont` and generates the fallback ahead of time, so nothing runs in the browser. In any Vite app (React, Svelte, Solid, Vue), add [`fontless`](https://github.com/unjs/fontaine/tree/main/packages/fontless): ```ts // vite.config.ts import react from '@vitejs/plugin-react' import { fontless } from 'fontless' import { defineConfig } from 'vite' export default defineConfig({ plugins: [react(), fontless()], }) ``` Then name a family in your CSS. The plugin scans for it, resolves it through `unifont`, and injects the `@font-face` rules and the metric-matched fallback: ```css h1 { font-family: "Poppins", sans-serif; } ``` In Nuxt, [`@nuxt/fonts`](https://fonts.nuxt.com) does the same thing. You can enable it like this: ```bash npx nuxt add fonts ``` If you'd rather not add a plugin at all, [Leturgerð](https://leturgero.web-runes.dev) is a CLI that downloads the files once and writes the `@font-face` rules and the fallback into your project: ```bash npx @leturgero/cli@latest ``` The result is plain font and CSS files that you commit, so it works with any tool rather than one bundler. ## Build it yourself If you're generating the CSS yourself, there are three steps: resolve the family, read the metrics of the file you're going to serve, and generate the fallback face. ```ts import { generateFontFace, readMetrics } from 'fontaine' import { createUnifont, providers } from 'unifont' const unifont = await createUnifont([providers.google()]) const { fonts, fallbacks } = await unifont.resolveFont('Poppins') const url = fonts .flatMap(font => font.src) .find(source => 'url' in source)?.url const metrics = url ? await readMetrics(url) : null const css = metrics ? generateFontFace(metrics, { name: 'Poppins fallback', font: fallbacks[0] ?? 'sans-serif' }) : '' ``` `generateFontFace()` expands the generic family for you: `local("Arial"), local("Arimo"), …` for `sans-serif`, so the rule matches something on most platforms. `resolveFont()` returns `fallbacks` for this reason. It's the generic the provider reports as closest to the family. Put the fallback between the real family and the generic, so text is set in the adjusted face until the webfont loads: ```css h1 { font-family: "Poppins", "Poppins fallback", sans-serif; } ``` ### Which metrics to use `readMetrics()` downloads the font file and measures it, which costs a request per family. Two cheaper sources exist, with a tradeoff each: - `getMetricsForFamily(family)` from `fontaine` looks the name up in a bundled dataset. No network, but the dataset covers the family as published, which may differ from the cut a provider serves. - The `metrics` on a resolved [`FontFaceData`](/docs/reference#fontfacedata), where the provider reports them. No network and no guessing, but few providers do, and the set is partial: `generateFontFace()` needs `xWidthAvg` to compute `size-adjust`, and a provider that omits it leaves the fallback unscaled. Whichever you pick, cache the result. Metrics for a given URL never change, so they belong in the same [storage](/docs/caching) as the rest of your font data. ### When a fallback won't help The adjusted face is built on `local()`, so it only applies if one of the named fonts is installed. Where nothing matches, the browser skips the rule and falls through to the generic, unadjusted. That's the usual outcome on a machine with no Microsoft or Apple system fonts, so treat the fallback as an improvement on most visits rather than a guarantee on all of them. ## Next steps - [Resolving fonts](/docs/resolving) covers the options that control which faces you get back. - [Caching](/docs/caching) covers where to keep resolved metadata between builds. --- # Browser and web containers > Why you can't reach provider APIs from a browser, and how the proxy handles it. No built-in provider API except `npm` sends CORS headers. From a browser, a request to `fonts.googleapis.com/css2?family=…` will fail. `unifont` spots that and sends provider requests through a hosted proxy instead, so the same code works with no configuration: ```ts import { createUnifont, providers } from 'unifont' // In a browser or a StackBlitz web container, this goes via the proxy. const unifont = await createUnifont([providers.google()]) ``` That covers browsers and web containers like StackBlitz, where the Node process runs in the browser and is bound by the same CORS rules. Everywhere else (a real Node server, a worker, your CI) the provider APIs are called directly and no proxy is involved. ## What actually gets proxied Only the provider API endpoints, and only the fixed list the proxy knows about. It isn't an open relay: - Provider metadata endpoints are rewritten to go through `apiBase`. - Everything else is fetched directly: npm CDNs, a custom provider's own API, the font files themselves. - With no `apiBase` set and no browser detected, nothing is rewritten. ## Pointing it somewhere else ```ts const unifont = await createUnifont([providers.google()], { apiBase: 'https://fonts.example.com', }) ``` Pass `false` to turn it off and always call the provider APIs directly: ```ts const unifont = await createUnifont([providers.google()], { apiBase: false }) ``` ## Don't use the public proxy in production `https://proxy.unifont.dev` is a convenience, and only that: it's there so reproductions, playgrounds and StackBlitz demos work without anyone deploying anything first. It's best-effort, it might be rate-limited, and it might change or disappear without warning. The proxy is designed to be self-hosted, and running your own takes a deploy and one option. [Self-hosting the proxy](/docs/proxy) covers what it will and won't do, the endpoints it serves, and where to put it. You only need any of this if you resolve fonts in a browser or a web container at runtime. If your fonts are resolved at build time (which is what `@nuxt/fonts` and `fontless` do) no proxy is involved. ## Behind a corporate proxy On the server, `unifont` honours `HTTPS_PROXY` and `HTTP_PROXY` (and their lower-case forms) for both metadata and font files, so builds work behind an egress proxy with no extra configuration. That's separate from `apiBase`. The two don't interact. --- # Self-hosting the proxy > What the proxy will and won't do, the endpoints it serves, and how to deploy your own. The proxy is a small Nitro app in the [`proxy/`](https://github.com/unjs/unifont/tree/main/proxy) directory of the `unifont` repository. Provider APIs send no CORS headers, so it's how [browsers and web containers](/docs/browser) reach them at all. Deploy your own if you resolve fonts in a browser or a web container at runtime and you need that to keep working. Nothing else needs it: on a server, in a worker, in CI, or at build time (what `@nuxt/fonts` and `fontless` do) the provider APIs are called directly. ## What it will and won't do It's read-only, and narrow on purpose: - `GET` and `HEAD` only, and only the routes below. Anything else is a `404`. - Only the query parameters named per route. Everything else is dropped before the upstream call. - Only the upstream `content-type` is carried back. No other upstream header is replayed. - No credentials, no cookies, no auth, in either direction. - Font files never go through it. They come from the provider's own CDN, which already allows cross-origin requests. ## Endpoints `GET /` returns the endpoint list as JSON. Every route is versioned: | Route | Upstream | Query | | --- | --- | --- | | `/adobe/v1/kit/:id` | `typekit.com/api/v1/json/kits/:id/published` | | | `/adobe/v1/kit-css/:id` | `use.typekit.net/:id.css` | | | `/bunny/v1/list` | `fonts.bunny.net/list` | | | `/bunny/v1/css` | `fonts.bunny.net/css` | `family` | | `/fontshare/v1/fonts` | `api.fontshare.com/v2/fonts` | `offset`, `limit` | | `/fontshare/v1/css` | `api.fontshare.com/v2/css` | `f[]` | | `/fontsource/v1/fonts` | `api.fontsource.org/v1/fonts` | | | `/fontsource/v1/fonts/:id` | `api.fontsource.org/v1/fonts/:id` | | | `/fontsource/v1/variable/:id` | `api.fontsource.org/v1/variable/:id` | | | `/google/v1/fonts` | `fonts.google.com/metadata/fonts` | | | `/google/v1/css` | `fonts.googleapis.com/css2` | `family`, `text`, `icon_names`, `format` | | `/google/v1/icons` | `fonts.google.com/metadata/icons` | | | `/google/v1/icon` | `fonts.googleapis.com/icon` | `family`, `format` | `format` is one of `woff2` (the default), `woff`, `ttf` or `eot`. Each one maps to the user agent Google expects for that format. Responses carry `cache-control` (`public`, `max-age` = `s-maxage`, plus `stale-while-revalidate`), an `etag`, and `304` support. Metadata is cached for six hours, generated CSS for a day, Adobe kits for five minutes. ## Deploying it Nothing here needs a database, credentials or environment variables. - **Vercel, Netlify, Cloudflare:** create a project with `proxy/` as the root directory. Nitro picks up the preset from the environment, and the build command is `pnpm build`. - **Node:** `pnpm build`, then run `.output/server/index.mjs`. Once it's up, `GET /` should return the endpoint list. Pointing `apiBase` at it is the only change your application needs: ```ts const unifont = await createUnifont([providers.google()], { apiBase: 'https://fonts.example.com', }) ``` A fresh deployment is cold, so the first request for each endpoint waits on the upstream. The cache lives in the process, which on a serverless host means most of the work is done by the CDN in front. That's what the `cache-control` headers are for. --- # Caching > Provider metadata barely changes. Cache it, and cold starts stop hurting. By default `unifont` caches in memory, which lasts as long as the process does. For a build tool or a server that's usually the wrong choice, because every restart pays the full round trip for the same `getFontProperties('Inter')` call. The `storage` option takes anything with `getItem` and `setItem`, so anything [`unstorage`](https://unstorage.unjs.io) can drive will work: ```ts import { createUnifont, providers } from 'unifont' import { createStorage } from 'unstorage' import fsDriver from 'unstorage/drivers/fs-lite' const storage = createStorage({ driver: fsDriver({ base: 'node_modules/.cache/unifont' }), }) const unifont = await createUnifont([providers.google()], { storage }) ``` Entries expire after a week, and they're keyed by `unifont` version, so upgrading the package throws them away rather than handing you data in an old shape. ## In Nitro or Nuxt Nitro already has storage, so give `unifont` a thin adapter over it: ```ts // server/utils/unifont.ts import { createUnifont, providers } from 'unifont' let instance: ReturnType | undefined export function useUnifont() { const cache = useStorage('unifont') instance ??= createUnifont([providers.google()], { storage: { getItem: key => cache.getItem(key), setItem: (key, value) => cache.setItem(key, value), }, }) return instance } ``` That's what this site does. Point the `unifont` bucket at the filesystem in development and at KV in production, and family pages come back warm. ## What is worth caching Providers cache two things behind the same interface. There's the family index built when a provider starts: `listFonts` data, large, and it hardly ever changes. And there are per-family results, keyed by the options you passed: small, but a lot of them. Both go through the storage you provide, so one driver covers it. Keep the instance around too, as the example above does. Starting a provider means fetching the whole family index for some of them, and that only happens once per instance. Creating one per request throws that work away. --- # Writing a provider > Wrap a foundry API, an internal font server, or a folder on disk. A provider is a function that returns up to three methods. `defineFontProvider()` gives it a name and types its options: ```ts import { defineFontProvider } from 'unifont' export interface FoundryOptions { token: string } export const foundry = defineFontProvider('foundry', async (options: FoundryOptions, ctx) => { // Runs once, when the instance is created. return { listFonts() { /* … */ }, getFontProperties(family) { /* … */ }, resolveFont(family, options) { /* … */ }, } }) ``` Use it like any built-in: ```ts const unifont = await createUnifont([ foundry({ token: process.env.FOUNDRY_TOKEN }), providers.google(), ]) ``` ## The context `unifont` passes two globally-configured primitives into each provider via a `ctx`. `ctx.fetch` retries failures and rewrites the built-in providers' URLs to [`apiBase`](/docs/browser). Your own endpoints aren't rewritten, so a custom provider works in a browser only if its API sends CORS headers or you proxy it yourself. `ctx.storage.getItem(key, init)` is a cache that fills itself. Pass a function and it runs once, then reads from whatever storage the user configured: ```ts export const foundry = defineFontProvider('foundry', async (options: FoundryOptions, ctx) => { const families = await ctx.storage.getItem('foundry:index.json', () => ctx.fetch('https://api.foundry.example/families').then(res => res.json()), ) // … }) ``` ## `listFonts()` Optional. Return the family names you can resolve, or `undefined` if listing doesn't make sense for your source. ```ts listFonts() { return families.map(family => family.name) } ``` ## `getFontProperties()` Optional. Return `undefined` for a family you don't know, and `unifont` will move on to the next provider. ```ts getFontProperties(family) { const match = families.find(item => item.name === family) if (!match) { return undefined } return { weights: match.weights, styles: match.styles, subsets: match.subsets, formats: ['woff2'], } } ``` ## `resolveFont()` The only method that's required for every provider. Return `undefined` for families you don't know. Otherwise return `fonts`, and `fallbacks` if you have them. Key your cache on the options, because the same family may resolve differently from one request to the next. Any stable hash will do: ```ts import { fnv1a64Base36 } from 'fnv1a-64' async resolveFont(family, options) { const match = families.find(item => item.name === family) if (!match) { return } return { fonts: await ctx.storage.getItem(`foundry:${family}-${fnv1a64Base36(JSON.stringify(options))}.json`, async () => { const css = await ctx.fetch(match.cssUrl).then(res => res.text()) return parseIntoFontFaceData(css) }), fallbacks: ['sans-serif'], } } ``` Most providers are really just a CSS parser. If your source hands you a stylesheet, read the built-in ones in [the repository](https://github.com/unjs/unifont/tree/main/src/providers): `google.ts` for subset-split woff2, `fontshare.ts` for whole-file families, `npm.ts` for local resolution. ## Variable axes If your API can deliver a font file instanced to the axis values in `options.variableAxis`, request it and report the values the file you return carries: ```ts async resolveFont(family, options) { options.variableAxis // { CASL: ['1'], wdth: [['75', '100']] } const applied = pickPublishedAxes(family, options.variableAxis) return { fonts: await fetchInstancedFaces(family, applied), appliedVariableAxis: applied, // { CASL: ['1'] } } } ``` `options.variableAxis` is normalised before it reaches you: a single value is a string, a range is a `[min, max]` pair of strings. Report the values the file really carries, which may be narrower than the request if you clamp to what a family publishes, and leave out any axis you could not apply. Don't report an axis you applied as a `font-variation-settings` descriptor. `unifont` turns this into the report the caller reads, and writes no descriptor of its own for a family you resolved, since you know which axes the file carries. Omit `appliedVariableAxis` when you serve whole variable files. `unifont` then expresses single-value axes as `font-variation-settings` on the faces you return. See [variable axes](/docs/resolving#variable-axes). ## Family options Declare a family options type and it's inferred at the call site, under your provider's name: ```ts export interface FoundryFamilyOptions { optical?: 'display' | 'text' } import type { ProviderResolveFontOptions } from 'unifont' export const foundry = defineFontProvider('foundry', async (options: FoundryOptions, ctx) => ({ async resolveFont(family, options: ProviderResolveFontOptions) { options.options?.optical // typed // … }, })) ``` ```ts await unifont.resolveFont('Foundry Sans', { options: { foundry: { optical: 'display' }, }, }) ``` ## Failing Providers start lazily, the first time a request reaches them. If a provider throws while starting up, it's skipped and the rest carry on, unless the user set `throwOnError` and no other provider could serve the family. Don't try to avoid that by returning empty data when something fails. An empty result means "I know this family and it has nothing", which stops anything else being asked. Throw, or return `undefined`. --- # API reference > Every export, option and signature in one place. Three runtime exports, one of them a namespace, and the types behind them: ```ts import { createUnifont, defineFontProvider, providers } from 'unifont' import type { FontFaceData, FontProperties, ResolveFontOptions, ResolveFontResult, Unifont } from 'unifont' ``` ## `createUnifont()` ```ts function createUnifont( providers: T, options?: UnifontOptions, ): Promise> ``` Providers are tried in order. The first one that knows a family answers. Providers start the first time a request reaches them, and some fetch a whole family index to do it. Keep the instance rather than making one per request. | Option | Type | Default | | | --- | --- | --- | --- | | `storage` | `{ getItem, setItem }` | in memory | [Caching](/docs/caching) | | `throwOnError` | `boolean` | `false` | see below | | `apiBase` | `string \| false` | the hosted proxy in a browser, otherwise unset | [Browser and web containers](/docs/browser) | By default a provider that fails to start, or throws from any of the three methods, is logged and skipped, and the next provider in the list is tried. Set `throwOnError` and you get the error instead, but for `resolveFont()` and `getFontProperties()` only when no provider could serve the request; failures a later provider recovered from are warnings. `listFonts()` merges every provider, so any failure there means an incomplete list and is thrown. Multiple failures are raised as an `AggregateError`. ## `resolveFont()` ```ts resolveFont( family: string, options?: Partial, providers?: string[] ): Promise ``` You get back `fonts`, the `@font-face` descriptors; `fallbacks`, the [generic families](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-family#generic-name) to put after yours in a stack; the `provider` that answered; and, when you asked for [variable axes](/docs/resolving#variable-axes), `variableAxis`, a record of what became of each axis you requested. Options are hints. Ask a provider that only serves woff2 for `otf` and you'll get woff2. | Option | Type | Default | | --- | --- | --- | | `weights` | `string[]` | `['400']` | | `styles` | `('normal' \| 'italic' \| 'oblique')[]` | `['normal', 'italic']` | | `subsets` | `string[]` | `['cyrillic-ext', 'cyrillic', 'greek-ext', 'greek', 'vietnamese', 'latin-ext', 'latin']` | | `formats` | `('woff2' \| 'woff' \| 'otf' \| 'ttf' \| 'eot')[]` | `['woff2']` | | `variableAxis` | `Partial>` | none | | `options` | `{ [provider: string]: Record }` | none | A weight range is one string with a space in it: `'100 900'`. The defaults above are exported as `defaultResolveOptions`. [Resolving fonts](/docs/resolving) explains what each option does to the answer. What goes in `options` is up to each provider, and the built-in ones are under [Providers](/docs/providers). ## `listFonts()` ```ts listFonts(providers?: string[]): Promise ``` Every family name the providers will list, or `undefined` if none of them can. `npm` and `adobe` never list. ## `getFontProperties()` ```ts getFontProperties( family: string, providers?: string[] ): Promise<(FontProperties & { provider?: string }) | undefined> ``` What a family publishes, from the first provider that knows it, and `undefined` when none of them do. `axes` lists the variation axes the family publishes, each as `{ tag, min, max, defaultValue }`, which is what you pass values for through [`variableAxis`](/docs/resolving#variable-axes). Google, Fontsource and Fontshare report them. Two things to watch when you read the result. `weights` mixes discrete values (`'400'`) with variable ranges (`'100 900'`). And a missing field means the provider doesn't publish that information, not that there's nothing to publish: `formats` in particular is what the provider can serve in general, so a format listed there may still be missing for the family you asked about. ## Asking only some providers All three methods take a list of provider names as their last argument. The default is every provider the instance was built with. ```ts const unifont = await createUnifont([providers.google(), providers.bunny()]) await unifont.resolveFont('Poppins', {}, ['bunny']) await unifont.getFontProperties('Poppins', ['bunny']) await unifont.listFonts(['bunny']) ``` ## `FontFaceData` ```ts interface FontFaceData { src: Array<{ name: string } | { url: string, format?: string, tech?: string }> display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional' weight?: string | number | [number, number] stretch?: string style?: string unicodeRange?: string[] featureSettings?: string variationSettings?: string metrics?: FontMetrics meta?: { priority?: number, subset?: string, init?: RequestInit } } ``` A `src` entry with `name` is a local font, one with `url` is remote. `meta.subset` is the provider's own name for the range. `meta.init` holds a `RequestInit` for the few providers whose files need headers. ## `FontMetrics` ```ts interface FontMetrics { unitsPerEm: number ascent?: number descent?: number lineGap?: number capHeight?: number xHeight?: number xWidthAvg?: number } ``` Values are in font units, using the same vocabulary as [`@capsizecss/metrics`](https://github.com/seek-oss/capsize), so they can be used to generate fallback metric overrides (`size-adjust`, `ascent-override`, ...) without downloading and parsing the font file. [Reducing layout shift](/docs/layout-shift) covers what to do with them. ## `defineFontProvider()` ```ts function defineFontProvider( name: Name, provider: (options: Options, ctx: ProviderContext) => Awaitable<{ resolveFont: (family: string, options: ProviderResolveFontOptions) => Awaitable getFontProperties?: (family: string) => Awaitable listFonts?: () => Awaitable } | undefined> ): (options?: Options) => Provider ``` `resolveFont()` is the only method you have to write. [Writing a provider](/docs/custom-providers) covers the rest. --- # About unifont > What unifont is, who maintains it, and what this site is for. `unifont` is an open-source JavaScript library for reading font metadata from font CDNs. Google Fonts returns a stylesheet you have to parse. Fontshare returns JSON, with its own names for weights. Fontsource publishes to npm. And Adobe needs a Typekit project id. `unifont` puts one interface over all of them: ask for a family and you'll get weights, styles, subsets, unicode ranges and `@font-face` data back in the same shape, whichever provider answered. It's deliberately small. It reads metadata and hands it to you. It doesn't download font files, subset them, generate a build pipeline, or cache anything on your behalf. ## Who makes it `unifont` is maintained by [Daniel Roe](https://roe.dev), with [qwerzl](https://github.com/qwerzl) and [florian-lefebvre](https://florian-lefebvre.dev), at [github.com/unjs/unifont](https://github.com/unjs/unifont). It's published to npm as [`unifont`](https://www.npmjs.com/package/unifont) under the MIT licence, and there's no company behind it and nothing to sign up for. It's used by [`@nuxt/fonts`](https://fonts.nuxt.com) and [`fontless`](https://github.com/unjs/fontaine/tree/main/packages/fontless), which wrap it to download, self-host and metric-match fonts automatically in Nuxt and Vite apps. If you want fonts to work on a site rather than to query font metadata yourself, those are the better starting point: [Reducing layout shift](/docs/layout-shift) shows how to set one up. ## What this site is unifont.dev is the project site, and it's powered end to end by the library it documents. The catalogue is `listFonts()`, each family page is `getFontProperties()` and `resolveFont()`, and every face on the site was resolved at build time. Every page here is also an HTTP endpoint. The [catalogue](/fonts), the [provider comparison](/compare) and each family page are thin layers over the [public API](/api), which needs no authentication. There's an MCP server at `/mcp` for agents, an [OpenAPI description](/openapi.json) of every endpoint, and a [machine-readable index](/llms.txt) of the site. Ask any prose page for `text/markdown`, or append `.md` to its path, and it answers in markdown. ## Font data and licensing Font metadata and font files belong to the foundries that made them and are licensed by them, not by us. This site never proxies or re-hosts font binaries: every file URL in a response points at the provider's own CDN. Check a family's licence with its foundry before shipping it. ## Where the catalogue comes from The catalogue is a cache of what each provider's own API answers, not a copy of it. Family indexes are read from the providers, held for an hour and refreshed in the background; measured transfer sizes are held for a week; nothing is kept beyond what makes the site answer quickly. A build primes the same cache so a cold deployment doesn't call five providers on its first request. Providers are asked in their documented ways, and no result is attributed to a provider that didn't answer it. The font CSS this site serves is for reading, copying and scripting, and for its own pages. It isn't a font CDN: a `` pointing here from another site is refused, because that would put someone else's traffic through our origin and through the providers' terms. Resolve the family with `unifont` and serve the CSS from your own origin. The public API is best-effort infrastructure for scripts, editor plugins and CI checks. It may be rate-limited or withdrawn, so if a deployment of yours depends on font resolution, install `unifont` and run it yourself, or [self-host the CORS proxy](/docs/proxy). ## Stacks live in your account A stack you publish is a `dev.unifont.stack` record written to your own atproto account, through OAuth against your own server. This site never holds it: it reads records back from the accounts that own them, and finds them through [constellation](https://constellation.microcosm.blue), a public index of links between records. Delete the record anywhere that speaks atproto and it is gone from here too, because there was never a second copy. The write happens in your browser, against your own server, so there is no session here to keep: the only thing this site publishes about it is the [client metadata document](/oauth-client-metadata.json) your server reads to identify the app. --- # Contact > How to report a bug, ask a question, or reach the maintainers of unifont. `unifont` is maintained on GitHub, so that is the fastest way to reach anyone who can help. Everything happens at [github.com/unjs/unifont](https://github.com/unjs/unifont), where you can see what's already been asked and answered. ## Report a bug Open an issue at [github.com/unjs/unifont/issues](https://github.com/unjs/unifont/issues). A useful report names the provider, the family, the options you passed, and what you expected instead. If a family resolves differently from what a provider's own site shows, include the provider name: metadata differs between CDNs more often than people expect, and the [comparison view](/compare) will usually show it. If the problem is with this site rather than the library, say so in the issue. The site lives in the `docs/` directory of the same repository, and the CORS proxy in `proxy/`. ## Ask a question Questions about how to resolve a family, cache the results, or write your own provider are welcome as [GitHub discussions or issues](https://github.com/unjs/unifont/issues). Check the [documentation](/docs) first: installing, [resolving fonts](/docs/resolving), [providers](/docs/providers), [caching](/docs/caching) and [writing a custom provider](/docs/custom-providers) each have a page. For questions about a font's licence, its glyph set, or its price, contact the foundry that made it. We only read what the CDNs publish and can't speak for the type designers whose work they distribute. ## Security If you've found a security problem in `unifont`, in this site, or in the hosted proxy, please report it privately through [GitHub security advisories](https://github.com/unjs/unifont/security/advisories) rather than opening a public issue, and we'll take a look. ## Contribute Pull requests are welcome, including new providers. Read [writing a custom provider](/docs/custom-providers) first: a provider is a small adapter, and adding one to the library is mostly a matter of tests. The project has a [code of conduct](https://github.com/unjs/unifont/blob/main/CODE_OF_CONDUCT.md). ## Maintainers `unifont` is maintained by [Daniel Roe](https://roe.dev), with [qwerzl](https://github.com/qwerzl) and [florian-lefebvre](https://florian-lefebvre.dev). --- # Privacy > What unifont.dev collects, what it does not, and what leaves this site when you use it. unifont.dev has no accounts, no sign-up, no newsletter and nothing to buy, so there's very little to say here. We don't want your data and we have nowhere to put it. ## No tracking, no cookies This site sets no cookies, uses no local storage for identification, and ships no analytics, tag manager, advertising or session-recording script. There's nothing to consent to because nothing is being collected about you. The pages are static or server-rendered HTML with a small amount of JavaScript for search and the font tester, all served from the same origin. ## What the server sees Like any web server, the host serving unifont.dev processes the requests it answers: the URL you asked for, the time, your IP address, your user agent and the referring page. These are the ordinary operational logs a hosting provider keeps to serve traffic, spot abuse and rate-limit it if necessary. They aren't joined to a profile, sold, shared with advertisers, or used to build any picture of you. The public [HTTP API](/api) and the [MCP server](/mcp) work the same way. Requests need no key, so a request carries no identity beyond what your HTTP client sends. Query parameters, such as a family name or a string you pass to the coverage endpoint, appear in those logs; don't send anything confidential in a URL. ## Third parties Font metadata is read from the CDNs (Google Fonts, Bunny Fonts, Fontshare, Fontsource, Google Icons, npm) server-side, from our server to theirs. Font files are a different matter. This site never proxies or re-hosts them, so the `@font-face` rules behind the specimens on the catalogue, the family pages and the comparison view point at the provider's own CDN, and your browser fetches them from there. Loading those pages tells the provider (usually Google) your IP address and user agent, exactly as any site using their CDN would. The typefaces the interface itself is set in are self-hosted here, so they cost no third-party request; contributor avatars in the footer are served by GitHub. ## The hosted proxy `unifont` running in a browser or a web container can't reach provider APIs directly, because they send no CORS headers, so it routes those requests through a proxy we host. The proxy forwards a provider request and returns the answer; it's described in the [self-hosting the proxy](/docs/proxy) documentation, and you can run your own if you'd rather no request of yours passed through ours. ## Your rights, and getting in touch Because we hold no personal data beyond transient server logs, there's normally nothing to export or erase. If you believe otherwise, or you have a question about any of the above, open an issue or contact us as described on the [contact page](/contact). If this page changes, the change will be in the [public git history](https://github.com/unjs/unifont) of this site along with everything else.