Use advanced typography with local fonts

Learn how the Local Font Access API allows you to access the user's locally installed fonts and obtain low-level details about them

Web safe fonts

If you have been doing web development long enough, you may remember the so-called web safe fonts. These fonts are known to be available on nearly all instances of the most used operating systems (namely Windows, macOS, the most common Linux distributions, Android, and iOS). In the early 2000s, Microsoft even spearheaded an initiative called TrueType core fonts for the Web that provided these fonts for free download with the objective that "whenever you visit a Web site that specifies them, you'll see pages exactly as the site designer intended". Yes, this included sites set in Comic Sans MS. Here is a classic web safe font stack (with the ultimate fallback of whatever sans-serif font) might look like this:

body {
  font-family: Helvetica, Arial, sans-serif;
}

Web fonts

The days where web safe fonts really mattered are long gone. Today, we have web fonts, some of which are even variable fonts that we can tweak further by changing the values for the various exposed axes. You can use web fonts by declaring an @font-face block at the start of the CSS, which specifies the font file(s) to download:

@font-face {
  font-family: 'FlamboyantSansSerif';
  src: url('flamboyant.woff2');
}

After this, you can then use the custom web font by specifying the font-family, as normal:

body {
  font-family: 'FlamboyantSansSerif';
}

Local fonts as fingerprint vector

Most web fonts come from, well, the web. An interesting fact, though, is that the src property in the @font-face declaration, apart from the url() function, also accepts a local() function. This allows custom fonts to be loaded (surprise!) locally. If the user happens to have FlamboyantSansSerif installed on their operating system, the local copy will be used rather than it being downloaded:

@font-face {
  font-family: 'FlamboyantSansSerif';
  src: local('FlamboyantSansSerif'), url('flamboyant.woff2');
}

This approach provides a nice fallback mechanism that potentially saves bandwidth. On the Internet, unfortunately, we cannot have nice things. The problem with the local() function is that it can be abused for browser fingerprinting. Turns out, the list of fonts a user has installed can be pretty identifying. A lot of companies have their own corporate fonts that are installed on employees' laptops. For example, Google has a corporate font called Google Sans.

The macOS Font Book app showing a preview of the Google Sans font.
The Google Sans font installed on a Google employee's laptop.

An attacker can try to determine what company someone works for by testing for the existence of a large number of known corporate fonts like Google Sans. The attacker would attempt rendering text set in these fonts on a canvas and measure the glyphs. If the glyphs match the known shape of the corporate font, the attacker has a hit. If the glyphs do not match, the attacker knows that a default replacement font was used since the corporate font was not installed. For full details on this and other browser fingerprinting attacks, read the survey paper by Laperdix et al.

Company fonts apart, even just the list of installed fonts can be identifying. The situation with this attack vector has become so bad that recently the WebKit team decided to "only include [in the list available fonts] web fonts and fonts that come with the operating system, but not locally user-installed fonts". (And here I am, with an article on granting access to local fonts.)

The Local Font Access API

The beginning of this article may have put you in a negative mood. Can we really not have nice things? Fret not. We think we can, and maybe everything is not hopeless. But first, let me answer a question that you might be asking yourself.

Why do we need the Local Font Access API when there are web fonts?

Professional-quality design and graphics tools have historically been difficult to deliver on the web. One stumbling block has been an inability to access and use the full variety of professionally constructed and hinted fonts that designers have locally installed. Web fonts enable some publishing use-cases, but fail to enable programmatic access to the vector glyph shapes and font tables used by rasterizers to render the glyph outlines. There is likewise no way to access a web font's binary data.

  • Design tools need access to font bytes to do their own OpenType layout implementation and allow design tools to hook in at lower levels, for actions such as performing vector filters or transforms on the glyph shapes.
  • Developers may have legacy font stacks for their applications that they are bringing to the web. To use these stacks, they usually require direct access to font data, something web fonts do not provide.
  • Some fonts may not be licensed for delivery over the web. For example, Linotype has a license for some fonts that only includes desktop use.

The Local Font Access API is an attempt at solving these challenges. It consists of two parts:

  • A font enumeration API, which allows users to grant access to the full set of available system fonts.
  • From each enumeration result, the ability to request low-level (byte-oriented) SFNT container access that includes the full font data.

Browser support

Browser Support

  • 103
  • 103
  • x
  • x

Source

How to use the Local Font Access API

Feature detection

To check if the Local Font Access API is supported, use:

if ('queryLocalFonts' in window) {
  // The Local Font Access API is supported
}

Enumerating local fonts

To obtain a list of the locally installed fonts, you need to call window.queryLocalFonts(). The first time, this will trigger a permission prompt, which the user can approve or deny. If the user approves their local fonts to be queried, the browser will return an array with fonts data that you can loop over. Each font is represented as a FontData object with the properties family (for example, "Comic Sans MS"), fullName (for example, "Comic Sans MS"), postscriptName (for example, "ComicSansMS"), and style (for example, "Regular").

// Query for all available fonts and log metadata.
try {
  const availableFonts = await window.queryLocalFonts();
  for (const fontData of availableFonts) {
    console.log(fontData.postscriptName);
    console.log(fontData.fullName);
    console.log(fontData.family);
    console.log(fontData.style);
  }
} catch (err) {
  console.error(err.name, err.message);
}

If you are only interested in a subset of fonts, you can also filter them based on the PostScript names by adding a postscriptNames parameter.

const availableFonts = await window.queryLocalFonts({
  postscriptNames: ['Verdana', 'Verdana-Bold', 'Verdana-Italic'],
});

Accessing SFNT data

Full SFNT access is available via the blob() method of the FontData object. SFNT is a font file format which can contain other fonts, such as PostScript, TrueType, OpenType, Web Open Font Format (WOFF) fonts and others.

try {
  const availableFonts = await window.queryLocalFonts({
    postscriptNames: ['ComicSansMS'],
  });
  for (const fontData of availableFonts) {
    // `blob()` returns a Blob containing valid and complete
    // SFNT-wrapped font data.
    const sfnt = await fontData.blob();
    // Slice out only the bytes we need: the first 4 bytes are the SFNT
    // version info.
    // Spec: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font
    const sfntVersion = await sfnt.slice(0, 4).text();

    let outlineFormat = 'UNKNOWN';
    switch (sfntVersion) {
      case '\x00\x01\x00\x00':
      case 'true':
      case 'typ1':
        outlineFormat = 'truetype';
        break;
      case 'OTTO':
        outlineFormat = 'cff';
        break;
    }
    console.log('Outline format:', outlineFormat);
  }
} catch (err) {
  console.error(err.name, err.message);
}

Demo

You can see the Local Font Access API in action in the demo below. Be sure to also check out the source code. The demo showcases a custom element called <font-select> that implements a local font picker.

Privacy considerations

The "local-fonts" permission appears to provide a highly fingerprintable surface. However, browsers are free to return anything they like. For example, anonymity-focused browsers may choose to only provide a set of default fonts built into the browser. Similarly, browsers are not required to provide table data exactly as it appears on disk.

Wherever possible, the Local Font Access API is designed to only expose exactly the information needed to enable the mentioned use cases. System APIs may produce a list of installed fonts not in a random or a sorted order, but in the order of font installation. Returning exactly the list of installed fonts given by such a system API can expose additional data that may be used for fingerprinting, and use cases we want to enable are not assisted by retaining this ordering. As a result, this API requires that the returned data be sorted before being returned.

Security and permissions

The Chrome team has designed and implemented the Local Font Access API using the core principles defined in Controlling Access to Powerful Web Platform Features, including user control, transparency, and ergonomics.

User control

Access to a user's fonts is fully under their control and will not be allowed unless the "local-fonts" permission, as listed in the permission registry, is granted.

Transparency

Whether a site has been granted access to the user's local fonts will be visible in the site information sheet.

Permission persistence

The "local-fonts" permission will be persisted between page reloads. It can be revoked via the site information sheet.

Feedback

The Chrome team wants to hear about your experiences with the Local Font Access API.

Tell us about the API design

Is there something about the API that does not work like you expected? Or are there missing methods or properties that you need to implement your idea? Have a question or comment on the security model? File a spec issue on the corresponding GitHub repo, or add your thoughts to an existing issue.

Report a problem with the implementation

Did you find a bug with Chrome's implementation? Or is the implementation different from the spec? File a bug at new.crbug.com. Be sure to include as much detail as you can, simple instructions for reproducing, and enter Blink>Storage>FontAccess in the Components box. Glitch works great for sharing quick and easy repros.

Show support for the API

Are you planning to use the Local Font Access API? Your public support helps the Chrome team to prioritize features and shows other browser vendors how critical it is to support them.

Send a tweet to @ChromiumDev using the hashtag #LocalFontAccess and let us know where and how you're using it.

Acknowledgements

The Local Font Access API spec was edited by Emil A. Eklund, Alex Russell, Joshua Bell, and Olivier Yiptong. This article was reviewed by Joe Medley, Dominik Röttsches, and Olivier Yiptong. Hero image by Brett Jordan on Unsplash.