Font Picker

<esp-font-picker> lets users browse and select from the full Google Fonts catalog by default. Font previews render in the dropdown using locally-served CSS files with base64-embedded font data, so no external requests are made while browsing preview rows.

For email editors, set font-source="web-safe" to offer only email-safe/system CSS font stacks and avoid Google font loading entirely. For site settings that should offer both, set font-source="all": the device stacks lead the list, followed by the Google catalog, and every row carries an icon naming its kind.


How It Works

In the default font-source="google" mode, the picker relies on generated Google Fonts assets.

The font tooling generates three kinds of assets with npm run build-fonts:

  1. font-definitions.json — A compact JSON catalog of every Google Font (family, category, variants, etc.). The component fetches this file once on first render to populate the dropdown.
  2. Individual CSS files — One <FontFamily>.css per font, each containing a @font-face rule with the preview weight base64-embedded. These are loaded on demand as the user scrolls through the list and unloaded when they scroll away.
  3. font-fallback-profiles.json — Variant-specific metric matches used by host builds to compile first-paint CSS. The picker never fetches this file, and a published site does not serve it to browsers.

All three assets live in the same directory (by default css/fonts/).

Selecting a Google font may still load a full runtime stylesheet from fonts.googleapis.com so the selected font renders beyond the preview subset. Use <esp-root google-font-loading="none"> to prevent those runtime Google Fonts CDN requests while keeping the local preview CSS available.

In font-source="web-safe" mode, the picker uses a built-in list of CSS font stacks and does not fetch font-definitions.json, preview CSS, or Google Fonts stylesheets.


Serving Font Assets

After running npm run build-fonts, copy or serve the css/fonts/ directory from a public path. Then tell <esp-root> where to find it:

<esp-root font-css-root="/css/fonts/">
  <!-- your app -->
</esp-root>

The font-css-root attribute defaults to /css/fonts/ and must end with a trailing slash. The font picker constructs CSS URLs by family name (e.g. {font-css-root}Roboto.css), so these files must not be renamed or hashed by your build pipeline.

By default the picker also loads font-definitions.json from the same directory. If your asset pipeline hashes filenames for cache busting, you can point the JSON at a hashed URL separately:

<esp-root font-css-root="/css/fonts/" font-definitions-url="/dist/hashed-catalog.json"> </esp-root>

When font-definitions-url is set, the picker fetches from that URL instead of {font-css-root}font-definitions.json.


Basic Usage

<esp-root font-css-root="/css/fonts/">
  <esp-form-item label="Body font">
    <esp-font-picker></esp-font-picker>
  </esp-form-item>
</esp-root>

Category Filtering

Restrict the picker to a specific font category:

<esp-font-picker category="monospace"></esp-font-picker>

Supported categories: serif, sans-serif, display, handwriting, monospace, not-display (everything except display fonts).

Pre-selected Value

<esp-font-picker value="Roboto"></esp-font-picker>

Web-safe Email Fonts

Use font-source="web-safe" when the selected value needs to be a complete CSS font stack that can be written directly into email markup:

<esp-form-item label="Email body font">
  <esp-font-picker font-source="web-safe"></esp-font-picker>
</esp-form-item>

Web-safe mode includes stacks such as Arial / Helvetica, Georgia / Times New Roman, Verdana / Geneva, Tahoma / Geneva, Trebuchet MS, Courier New, system sans, system serif, and system monospace. The picker item labels are human-readable, but .value is the complete CSS stack, for example Arial, Helvetica, sans-serif.

Device Fonts Alongside the Catalog

Use font-source="all" when a theme may store either a Google family or a device font stack, such as a theme authored outside the picker:

<esp-form-item
  label="Body font"
  hint="System fonts use the reader's device fonts and download nothing."
>
  <esp-font-picker font-source="all" value='system-ui, "Segoe UI", sans-serif'></esp-font-picker>
</esp-form-item>

The combined list leads with System Sans, System Serif, and System Monospace, then the classic web-safe stacks, then the catalog. Every row carries a kind icon in the picker's icon rail: a desktop for a device font, Google's mark for a catalog font. On a pointer device the icon's tooltip reads "Device font, no download" or "Google font"; the icon alone carries the distinction on touch screens. The other sources show no kind icon. value accepts either form:

  • A catalog family (Roboto), or a stack that leads with one ("Roboto", sans-serif), selects the Google font and loads its preview and runtime assets as in Google mode. A stack such as Roboto, system-ui, sans-serif is Roboto, not a device stack.
  • A device stack selects the preset it is equivalent to by its leading family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif shows System Sans and ui-monospace, "SFMono-Regular", Consolas, monospace shows System Monospace. A device stack that matches no preset shows as its own selected custom row, labeled with the stack.

Recognition never rewrites value. The stored stack stays exactly as given until the user chooses a row, and only that choice emits esp-value-changed. A device selection loads no preview CSS and creates no fonts.googleapis.com link, and device rows scrolled into view fetch nothing.

The same recognition is available to consumers as matchWebSafeFont, customWebSafeFont, isDeviceFontStack, classifyFontStack, and parseFontStack from the package's main entry.

The example starts with system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, which the picker recognizes as System Sans. Choose a row to see the value and esp-value-changed payload.

Picker value:

Last esp-value-changed: none yet; recognition alone never emits

The quick brown fox jumps over the lazy dog.

Weights for device fonts

extractWeights accepts the picker's event detail for either kind of font. The System Sans and System Serif presets, and custom stacks that lead with a platform UI face, report 300, 400, 500, 600, and 700; System Monospace, the classic web-safe stacks, and other custom stacks report 400 and 700. These are offers rather than guarantees: the reader's platform snaps to the nearest weight its face carries (SF, Segoe UI Variable, and Roboto carry the full range; Consolas and Courier New carry regular and bold) and may synthesize bold where none exists, so a weight picker for a device font should say so.

Listening for Changes

const picker = document.querySelector("esp-font-picker");
picker.addEventListener("esp-value-changed", (e) => {
  console.log("Selected font:", e.detail.family);
});

In the default Google mode, the esp-value-changed event's detail contains the full GoogleFont object for the selected font. In web-safe and combined modes a device selection's detail is the web-safe font object with family, stack, category, kind: "web-safe", and its offered weights. Branch on kind: write detail.stack verbatim for a device font and build the quoted family plus category fallback only for a Google font.


Font & Weight Demo

This demo shows how to pair <esp-font-picker> with a weight picker to set page fonts and weights in real time. Select a font and weight below to see the sample text update.

The quick brown fox jumps over the lazy dog.

Pack my box with five dozen liquor jugs. How vexingly quick daft zebras jump! Sphinx of black quartz, judge my vow.


Keeping Fonts Updated

Google regularly adds new fonts to their catalog. To keep your local assets current:

Manual update:

GOOGLE_FONTS_API_KEY="..." \
npm run build-fonts

GOOGLE_FONTS_API_KEY is required. Keep it in your local environment or a secret manager; the font builder intentionally does not include a fallback key.

The refresh also updates license/GOOGLE_FONTS_NOTICES.md with source metadata links and per-family notices. Families without authoritative google/fonts license metadata are excluded from the generated preview catalog until their provenance can be resolved.

Automated (GitHub Actions): The repository includes a weekly workflow (.github/workflows/update-fonts.yml) that runs npm run build-fonts every Monday and opens a pull request if new fonts are detected. To use it, add a GOOGLE_FONTS_API_KEY secret to your repository.


Runtime Font Loading

Local preview CSS and runtime font loading are separate. The generated css/fonts/*.css files are local preview subsets. Runtime Google font loading is controlled by <esp-root google-font-loading>.

<esp-root google-font-loading="none" font-css-root="/css/fonts/">
  <esp-form-item label="Body font">
    <esp-font-picker></esp-font-picker>
  </esp-form-item>
</esp-root>

google-font-loading="none" prevents Espalier from creating fonts.googleapis.com stylesheet links for theme fonts or selected <esp-font-picker> values. This mode is intended for offline deployments, strict CSP environments, privacy-sensitive screens, and enterprise customers who self-host approved production fonts. Use font-source="web-safe" when the picker itself should offer only system/email-safe font stacks.

Static production font plans

Static sites should compile selected fonts before rendering pages instead of using runtime Google stylesheet links. Import the dependency-free compiler and the generated profile catalog in the host build, discover the exact requested faces, resolve only those WOFF2 files, and compile again with the source map:

import { compileFontPlan, fontFaceRequestKey } from "@taprootio/espalier/shared/font-plan";
import profiles from "@taprootio/espalier/css/fonts/font-fallback-profiles.json" with { type: "json" };

const discovery = compileFontPlan({
  lightTheme,
  darkTheme,
  catalog: profiles,
});

// The host fetches or copies only discovery.requests, then supplies the
// resolved WOFF2 URLs. This example shows two already-resolved faces.
const plan = compileFontPlan({
  lightTheme,
  darkTheme,
  catalog: profiles,
  sources: {
    [fontFaceRequestKey("Noto Sans", 400)]: {
      url: "/assets/fonts/noto-sans-400.woff2",
    },
    [fontFaceRequestKey("Inika", 700)]: {
      url: "/assets/fonts/inika-700.woff2",
    },
  },
});

Embed plan.css in the existing inline style block in every page head and set google-font-loading="none". plan.requests is deduplicated across body, heading, brand, and monospace slots in both schemes; unused catalog families and variants never enter the page. plan.missingSources and plan.missingProfiles must be handled by the host build rather than ignored.

Each synthetic fallback face is sourced entirely from local() names, so it costs no request — but it only helps where the visitor's machine has one of those faces. The catalog therefore lists the metric-compatible Liberation face alongside the Microsoft core name on every base that needs it: Liberation Sans for the Arial base, Liberation Serif for Times New Roman, and Liberation Mono for Courier New. That is what keeps serif body text and monospace code from first painting in an unadjusted generic font, and then reflowing, on a Linux visitor who has no Microsoft core fonts installed.

The default plan targets the page's esp-root. If one document contains roots with different themes, compile each pair with a unique id selector such as rootSelector: "#marketing-root" and put that id on the corresponding root. The compiler accepts only a single id selector, so selector or style injection cannot enter the generated head CSS.

The same call supports Google-hosted and self-hosted production output. Supply selected fonts.gstatic.com WOFF2 URLs for the former, or own-origin/CDN URLs for the latter. The fallback metrics and effective stacks remain identical. Self-hosted output contains no Google host. Update font-src CSP from the chosen URLs, and ensure the page's inline-style nonce, hash, or policy covers the embedded plan.

Taproot should compute this plan once in its shared site context, insert the minified CSS into the existing page-head style in page.vto, resolve only the returned requests, and render <esp-root google-font-loading="none">.

Google picker rows derive the same deterministic fallback aliases and weight normalization without fetching the profile catalog in the browser. When the selected family is part of the site's compiled plan, its picker stack therefore uses the already-declared metric-matched faces; other rows retain their local preview subset.


Configuration Summary

Attribute / Property Element Description
font-css-root <esp-root> Path to the directory containing font CSS files. Must use stable (unhashed) filenames. Default: /css/fonts/
font-definitions-url <esp-root> Optional URL to font-definitions.json. When set, overrides the default {font-css-root}font-definitions.json path. Useful for cache-busted asset pipelines.
google-font-loading <esp-root> Runtime Google Fonts loading policy. auto allows selected Google fonts and theme fonts to load full stylesheets from Google. none prevents those CDN links.
category <esp-font-picker> Filter fonts by category (serif, sans-serif, monospace, etc.)
font-source <esp-font-picker> Font catalog source. google uses generated Google Fonts assets. web-safe uses built-in email-safe/system font stacks. all lists the device stacks above the catalog. Default: google
value <esp-font-picker> The currently selected Google font family or web-safe CSS font stack. In all mode either form; an equivalent device stack shows its preset without being rewritten
placeholder <esp-font-picker> Placeholder text for the input. Default: "Choose a font..."
Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O