# Local Font Access API

> window.queryLocalFonts() lists installed fonts as FontData objects behind a local-fonts permission. Options, FontData members, exceptions, examples.

`window.queryLocalFonts()` resolves with an array of `FontData` objects describing the fonts installed on the user's device, after a `local-fonts` permission prompt. Each `FontData` exposes the PostScript, full, family, and style names and a `blob()` method that returns the raw SFNT bytes, which is what design tools need to shape text with a user's own fonts rather than a web font.

Support is desktop Chromium only: Chrome 103 and the Edge and Opera releases built on it. Chrome on Android, Firefox, and Safari have no implementation in any version (BCD `api.Window.queryLocalFonts`), and the method is `[SecureContext]`, so it is also absent on `http://` origins other than `localhost`.

## Syntax

```js
window.queryLocalFonts()
window.queryLocalFonts(options)

fontData.blob()
```

`queryLocalFonts()` returns a `Promise<sequence<FontData>>` sorted in ascending order by `postscriptName`. `blob()` returns a `Promise<Blob>` whose `type` is `application/octet-stream`. The specification says the browser is not required to report every installed font, so the result is the set the user agent is willing to expose, filtered by the user's choice when the browser shows a picker.

## Parameters

`queryLocalFonts()` takes one optional `options` argument, a `QueryOptions` dictionary with a single member.

| Member | Type | Required | Description |
|---|---|---|---|
| `postscriptNames` | `sequence<DOMString>` | No | Only fonts whose PostScript name is in this list are returned, for example `["Verdana-Bold", "Arial"]`. An empty list returns nothing; an absent member returns every selectable font. Matching is exact, so `"Verdana Bold"` (with a space) matches nothing. |

Each returned `FontData` has four read-only `USVString` attributes and one method.

| Member | Type | Description |
|---|---|---|
| `postscriptName` | `USVString` | The PostScript name, such as `"Arial-Bold"`; also the sort key of the result. |
| `fullName` | `USVString` | Family plus subfamily, such as `"Arial Bold"`. |
| `family` | `USVString` | The family name as CSS `font-family` would use it, such as `"Arial"`. |
| `style` | `USVString` | The subfamily or style name, such as `"Regular"` or `"Bold Italic"`. |
| `blob()` | `Promise<Blob>` | The font file bytes (SFNT container: TrueType, OpenType, WOFF, or WOFF2). |

Names are returned as single strings in the US English or user-language localisation of the font's `name` table; a page that needs another language has to parse the `name` table from `blob()` itself.

## Exceptions

`queryLocalFonts()` rejects with the following `DOMException` names, in the order the specification checks them.

| Exception | Condition |
|---|---|
| `SecurityError` | The document's origin is opaque (for example a sandboxed iframe without `allow-same-origin`); or the document is not allowed to use the `local-fonts` policy-controlled feature (default allowlist `'self'`); or the call has no transient user activation. |
| `NotAllowedError` | The user denied the `local-fonts` permission prompt. |

`blob()` defines no rejections. Chromium adds rejections the specification does not list: `NotSupportedError` with `Not yet supported on this platform.` where the enumeration backend is missing, `SecurityError` with `Page needs to be visible.` when the tab is hidden, `DataError` with `Font data exceeds memory limit.`, and `UnknownError` for any other backend failure (see the observed callout for the denied-permission case, which Chromium also handles differently).

:::observed
In Chrome, `queryLocalFonts()` called from a timer rather than a click rejects with `SecurityError: User activation is required.`; blocked by Permissions Policy it throws `SecurityError: Access to the feature "local-fonts" is disallowed by Permissions Policy`. The permission bubble lists `Use the fonts on your computer so you can create high-fidelity content` (English UI) under the "This site would like to:" heading. When the user clicks Block, Chrome does not reject with `NotAllowedError` as the specification requires: `font_access.cc` resolves the promise with an empty array (`// Return an empty font list if user has denied the permission request.`), so a page cannot tell a denial from a device with no selectable fonts. Source: [`font_access.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/font_access/font_access.cc) (chromium.googlesource.com) and [`permissions_strings.grdp`](https://chromium.googlesource.com/chromium/src/+/main/components/permissions_strings.grdp) (chromium.googlesource.com).
:::

## Examples

Both examples run from a `click` handler, detect `queryLocalFonts` on `window`, and fall back to the fonts the page ships itself when the API is missing or the user declines.

### Filling a font picker from the device, with a bundled fallback list

The fallback list is the set of web fonts the page already loads, so the picker still has entries in Firefox, Safari, and mobile Chrome. An empty result in Chrome is treated the same way because of the denied-permission behaviour described above.

```js
const BUNDLED = ['Inter', 'Source Serif 4', 'JetBrains Mono'];

async function listFontFamilies() {
  if (!('queryLocalFonts' in window)) return BUNDLED;

  try {
    const fonts = await window.queryLocalFonts();
    if (fonts.length === 0) return BUNDLED; // denied in Chrome, or nothing selectable
    return [...new Set(fonts.map((f) => f.family))];
  } catch (err) {
    if (err.name === 'SecurityError' || err.name === 'NotAllowedError') return BUNDLED;
    throw err;
  }
}

document.querySelector('#pick-fonts').addEventListener('click', async () => {
  const select = document.querySelector('#font-family');
  select.replaceChildren(...(await listFontFamilies()).map((name) => new Option(name)));
});
```

Deduplicating on `family` matters because every weight and style of a family arrives as its own `FontData`: `"Arial"`, `"Arial-Bold"`, `"Arial-Italic"`, and `"Arial-BoldItalicMT"` are four entries sharing the family `"Arial"`.

### Checking for one specific font before rendering a document with it

`postscriptNames` narrows the prompt and the result to the fonts you care about. When the font is absent, the document is rendered with its embedded web font instead, and the user is told why the layout may differ.

```js
async function hasLocalFont(postscriptName) {
  if (!('queryLocalFonts' in window)) return false;
  try {
    const matches = await window.queryLocalFonts({ postscriptNames: [postscriptName] });
    return matches.length === 1;
  } catch {
    return false; // no gesture, policy block, or user declined
  }
}

document.querySelector('#open-document').addEventListener('click', async () => {
  const useLocal = await hasLocalFont('Verdana-Bold');
  document.body.style.fontFamily = useLocal ? '"Verdana"' : '"Verdana Web", sans-serif';
  document.querySelector('#font-note').hidden = useLocal;
});
```

`blob()` is only needed when you must read glyph outlines or the `name` table yourself (a canvas text editor, a font inspector); CSS can reference an installed font by family name without it.

## See also

- [File System Access API](/reference/capabilities/file-system-access/), the other desktop-Chromium file-level capability with a per-call user gesture
- [Font and image optimization: font-display, picture, and AVIF/WebP](/reference/performance/fonts-images/), for the web-font fallback path
- [PWAs on desktop](/reference/platforms/desktop/)
- [Local Font Access API: queryLocalFonts() method](https://wicg.github.io/local-font-access/#dom-window-querylocalfonts) (wicg.github.io)
- [Local Font Access API: local-fonts permission](https://wicg.github.io/local-font-access/#permissiondef-local-fonts) (wicg.github.io)
- [Window: queryLocalFonts() browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Window/queryLocalFonts#browser_compatibility) (developer.mozilla.org)