@molecule/app-fonts

Core interface · fonts · App (browser) · v1.2.1 · Apache-2.0

Font loading and typography — 20 self-hosted families ship as provider bonds (check here before npm-installing a webfont)

npm install @molecule/app-fonts

npm · Source on GitHub

How it works

@molecule/app-fonts is the fonts core interface on the app (browser) side: the API your app calls, with no vendor inside.

Choose the implementation by bonding one of its 20 providers: @molecule/app-fonts-arimo, @molecule/app-fonts-dm-sans, @molecule/app-fonts-dm-serif-display, @molecule/app-fonts-ibm-plex-mono, @molecule/app-fonts-ibm-plex-sans, @molecule/app-fonts-inter, @molecule/app-fonts-ionicons, @molecule/app-fonts-jetbrains-mono, @molecule/app-fonts-lato, @molecule/app-fonts-lora, @molecule/app-fonts-manrope, @molecule/app-fonts-material-symbols, @molecule/app-fonts-merriweather, @molecule/app-fonts-newsreader, @molecule/app-fonts-playfair-display, @molecule/app-fonts-plus-jakarta-sans, @molecule/app-fonts-source-sans-3, @molecule/app-fonts-source-serif-4, @molecule/app-fonts-space-grotesk, @molecule/app-fonts-system.

import { setFont } from '@molecule/app-fonts'
import { font as sans } from '@molecule/app-fonts-inter'
import { font as mono } from '@molecule/app-fonts-jetbrains-mono'

setFont(sans) // once, at app startup — before first paint
setFont(mono)

Providers (20): @molecule/app-fonts-arimo, @molecule/app-fonts-dm-sans, @molecule/app-fonts-dm-serif-display, @molecule/app-fonts-ibm-plex-mono, @molecule/app-fonts-ibm-plex-sans, @molecule/app-fonts-inter, @molecule/app-fonts-ionicons, @molecule/app-fonts-jetbrains-mono, @molecule/app-fonts-lato, @molecule/app-fonts-lora, @molecule/app-fonts-manrope, @molecule/app-fonts-material-symbols, @molecule/app-fonts-merriweather, @molecule/app-fonts-newsreader, @molecule/app-fonts-playfair-display, @molecule/app-fonts-plus-jakarta-sans, @molecule/app-fonts-source-sans-3, @molecule/app-fonts-source-serif-4, @molecule/app-fonts-space-grotesk, @molecule/app-fonts-system

Works with: @molecule/app-bond

Reference

Font interfaces and provider system for molecule.dev.

Registers a font per role (sans, serif, mono) and applies it app-wide: setFont() bonds the definition, sets the --mol-font-{role} CSS variable, and injects the loading CSS (<link> for CDN sources, @font-face for local files) into the document head. Font bond packages (e.g. @molecule/app-fonts-inter, @molecule/app-fonts-jetbrains-mono) export a ready-made FontDefinition named font.

Before adding a font, check the font bonds — the fleet ships 20 families. The providers under @molecule/app-fonts-* cover arimo, dm-sans, dm-serif-display, ibm-plex-mono, ibm-plex-sans, inter, ionicons, jetbrains-mono, lato, lora, manrope, material-symbols, merriweather, newsreader, playfair-display, plus-jakarta-sans, source-sans-3, source-serif-4, space-grotesk and system. source-sans-3 IS Source Sans Pro (Adobe renamed the typeface), so a stylesheet that asks for 'Source Sans Pro' is served by @molecule/app-fonts-source-sans-3. Do NOT npm install a webfont package (fontsource, typeface-*, a family's npm package such as source-sans-pro) for a family on this list: the bond is the font and wiring it is one setFont(font) call, while an install in the sandbox re-resolves the whole tree (~90 s) and nests node_modules at the workspace root, so a relative path from the app directory misses it.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setFont } from '@molecule/app-fonts'
import { font as sans } from '@molecule/app-fonts-inter'
import { font as mono } from '@molecule/app-fonts-jetbrains-mono'

setFont(sans) // once, at app startup — before first paint
setFont(mono)

Type

core

Installation

npm install @molecule/app-fonts @molecule/app-bond

API

Interfaces

FontFaceDescriptor

Descriptor for a single @font-face declaration (local fonts).

interface FontFaceDescriptor {
  /**
   * Font file: a filename relative to the package's fontsDir
   * (e.g., 'Arimo-Regular.ttf'), or an already-final URL — a root-absolute
   * asset path (`/assets/Arimo-1a2b.ttf`), a full `http(s):` URL or a
   * `data:` URL — which `setFont()` uses as-is. That is the shape a bundler
   * emits for `import ttf from '…Arimo-Regular.ttf?url'`.
   */
  file: string
  /** CSS font-weight value — number (400) or range string ('100 1000'). */
  weight: number | string
  /** CSS font-style: 'normal' or 'italic'. Defaults to 'normal'. */
  style?: string
  /** If true, this is a variable font — wrap in `@supports` (font-variation-settings: normal). */
  variable?: boolean
}

FontSource

Font loading source information.

interface FontSource {
  /** The source type for loading. */
  type: FontSourceType
  /** Full URL for the font CSS stylesheet (for google/bunny/custom). */
  url?: string
  /** Preconnect origins (e.g., ['https://fonts.googleapis.com', 'https://fonts.gstatic.com']). */
  preconnect?: string[]
  /** `@font-face` descriptors for local font files. */
  faces?: FontFaceDescriptor[]
}

FontDefinition

Complete definition for a single font family.

interface FontDefinition {
  /** CSS font-family name (e.g., 'Arimo', 'Inter'). */
  family: string
  /** Which role this font fills (sans, serif, or mono). */
  role: FontRole
  /** Fallback fonts appended after the family in the CSS value. */
  fallbacks: string[]
  /** How to load this font. */
  source: FontSource
  /**
   * Extra CSS injected verbatim after this font's `@font-face` block — the
   * utility class an icon font needs (e.g. `.material-symbols-outlined { … }`).
   * Only meaningful for `icon`-role fonts; ignored for text roles.
   */
  utilityCss?: string
}

SetFontOptions

Options for setFont().

interface SetFontOptions {
  /**
   * The public base path the app is served under — Vite's `base`
   * (`import.meta.env.BASE_URL`), `/blog/` for a site under a sub-path.
   * Local faces are loaded from `<basePath>fonts/<file>`. When omitted the
   * document's `<base href>`, then `import.meta.env.BASE_URL`, then `/` apply.
   */
  basePath?: string
}

FontConfig

Resolved font configuration for all three roles.

interface FontConfig {
  sans: FontDefinition
  serif: FontDefinition
  mono: FontDefinition
}

Types

FontSourceType

How a font is loaded.

type FontSourceType = 'google' | 'bunny' | 'local' | 'system' | 'custom'

FontRole

Which font role this font fills.

sans/serif/mono are text roles applied app-wide via --mol-font-{role}. icon is for icon fonts (e.g. Material Symbols) — consumed through a utility class (.material-symbols-outlined), not a text font-family, so an icon font carries utilityCss that setFont() injects alongside its @font-face.

type FontRole = 'sans' | 'serif' | 'mono' | 'icon'

Functions

resolveFontBasePath(basePath)

Resolves the public base path local font files are served under, always with a leading and a trailing slash (/, /blog/).

Order: the explicit basePath → the document's <base href> → Vite's import.meta.env.BASE_URL (the base option, present in Vite-built and Vite-served code) → /. A site served from a sub-path (base: '/blog/') has its public/fonts/ at /blog/fonts/, so a root-absolute /fonts/… 404s there and the fallback stack renders instead.

function resolveFontBasePath(basePath?: string): string
  • basePath — An explicit base, e.g. import.meta.env.BASE_URL or /blog.

Returns: The normalized base path.

isAbsoluteFontFileUrl(file)

Whether a local face's file is already a final URL, so setFont() must use it as-is instead of resolving it under <basePath>fonts/.

True for a root-absolute path (/assets/ionicons-1a2b.woff), a full URL (https:, http:, //host/…) or an inline data: URL. Those are exactly the shapes a bundler emits for import woff from '…woff?url'.

function isAbsoluteFontFileUrl(file: string): boolean
  • file — The face's file field.

Returns: true when the value is already a URL.

resolveFontFaceUrl(file, basePath)

Resolves a local face's file to the URL its @font-face loads from.

An already-final URL is returned unchanged (including a Vite asset URL that already carries the configured base). A plain filename is served from <basePath>fonts/<file>.

function resolveFontFaceUrl(file: string, basePath?: string): string
  • file — The face's file field.
  • basePath — Public base path from resolveFontBasePath (default /).

Returns: The URL to put in src: url(…).

setFont(font, options)

Registers a font definition for a specific role (sans, serif, or mono).

Bonds the font via the named provider system, sets the --mol-font-{role} CSS variable, and injects font loading into the document head (CDN <link> tags or @font-face declarations).

function setFont(font: FontDefinition, options?: SetFontOptions): void
  • font — The font definition including family, role, and source configuration.
  • options — basePath: the public base the app is served under (pass import.meta.env.BASE_URL in a Vite app); local faces load from <basePath>fonts/<file>. See resolveFontBasePath for the default.

getFont(role)

Retrieves the font definition bonded for a specific role.

function getFont(role: FontRole): FontDefinition | undefined
  • role — The font role ('sans', 'serif', or 'mono').

Returns: The bonded font definition, or undefined if none is set.

hasFont(role)

Checks whether a font has been bonded for a specific role.

function hasFont(role: FontRole): boolean
  • role — The font role to check.

Returns: true if a font is bonded for the given role.

getFontConfig()

Returns the complete font configuration for all three roles. Falls back to system font stacks for any role not explicitly set.

function getFontConfig(): FontConfig

Returns: The resolved font config with sans, serif, and mono definitions.

hasFontConfig()

Checks whether any font role has been explicitly configured.

function hasFontConfig(): boolean

Returns: true if at least one font role is bonded.

resetFonts()

Removes all bonded font definitions. Primarily used in tests.

function resetFonts(): void

buildFontFamily(font)

Build a CSS font-family value from a FontDefinition.

function buildFontFamily(font: FontDefinition): string
  • font — The font definition containing a primary family and fallback list.

Returns: A CSS-ready font-family string (e.g. "'Arimo', system-ui, sans-serif").

Constants

systemSans

System sans-serif font stack (used when no provider is configured).

const systemSans: FontDefinition

systemSerif

System serif font stack (used when no provider is configured).

const systemSerif: FontDefinition

systemMono

System monospace font stack (used when no provider is configured).

const systemMono: FontDefinition

Available Providers

ProviderPackage
Arimo@molecule/app-fonts-arimo
DM Sans@molecule/app-fonts-dm-sans
DM Serif Display@molecule/app-fonts-dm-serif-display
IBM Plex Mono@molecule/app-fonts-ibm-plex-mono
IBM Plex Sans@molecule/app-fonts-ibm-plex-sans
Inter@molecule/app-fonts-inter
Ionicons@molecule/app-fonts-ionicons
JetBrains Mono@molecule/app-fonts-jetbrains-mono
Lato@molecule/app-fonts-lato
Lora@molecule/app-fonts-lora
Manrope@molecule/app-fonts-manrope
Material Symbols Outlined@molecule/app-fonts-material-symbols
Merriweather@molecule/app-fonts-merriweather
Newsreader@molecule/app-fonts-newsreader
Playfair Display@molecule/app-fonts-playfair-display
Plus Jakarta Sans@molecule/app-fonts-plus-jakarta-sans
Source Sans Pro (Source Sans 3)@molecule/app-fonts-source-sans-3
Source Serif 4@molecule/app-fonts-source-serif-4
Space Grotesk@molecule/app-fonts-space-grotesk
System@molecule/app-fonts-system

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-bond ^1.0.1

Runtime Dependencies

  • @molecule/app-bond

  • ALL fonts are local — NEVER load one from a CDN. Every provider bond here (@molecule/app-fonts-inter, -jetbrains-mono, -material-symbols, and the display/serif faces) is source: 'local' and self-hosts from /fonts/. Do NOT add a Google Fonts <link href="https://fonts.googleapis.com…">, an @import url(https://fonts.gstatic…), or a next/font/google import: external fonts fail in the sandbox (egress is blocked), leak the visitor's IP, and add render-blocking round-trips (cross-site CDN caching died with browser cache partitioning ~2020). If an imported app loads fonts from a CDN, localize them — swap to one of these packages, or bundle the file + a local @font-face.

  • Consume fonts through the CSS variable (var(--mol-font-sans) etc.), never a hardcoded font-family — the variable is the swap point; hardcoding defeats the bond. The app's theme/ClassMap layer normally references it already.

  • Local-source fonts load from <basePath>fonts/<file> unless the face's file is already a URL. A file starting with /, //, http or data: is used as-is — that is what a bundler's asset import produces (import woff from '@molecule/app-fonts-ionicons/fonts/ionicons.woff?url'), so an app that cannot copy files into public/ can still bond a font package. Every local font provider package ships a ./fonts/* export for exactly that import. Any other file is a filename that must be served at <basePath>fonts/ (copy the bond's fonts/ into the app's public/fonts/) or every face silently 404s and the fallback stack renders instead. CDN sources inject <link> tags (with preconnect origins).

  • A site served under a sub-path (Vite base: '/blog/', BASE_PATH) MUST pass the base: setFont(font, { basePath: import.meta.env.BASE_URL }). Its public/fonts/ is at /blog/fonts/, so a root-absolute /fonts/… 404s on every page — and an e2e console guard fails every test on that 404. Without the option setFont() falls back to the document's <base href>, then import.meta.env.BASE_URL where the bundler defines it, then /; passing it is what makes the path certain in both dev and the built site.

  • Call setFont() at startup, before first paint, to avoid a font flash; calling again for the same role replaces the font (idempotent per role).

  • getFontConfig() always returns a complete config — roles never explicitly set fall back to system stacks (systemSans/systemSerif/systemMono).

  • Icon fonts (role icon, e.g. @molecule/app-fonts-material-symbols) are local like any other font — never load them from a CDN. They carry a utilityCss string (the .material-symbols-outlined class) that setFont() injects with the @font-face, so the icon glyphs work fully offline. NEVER add a <link href="https://fonts.googleapis.com/...Material+Symbols..."> — it fails in the sandbox (blocked egress) and leaks the visitor's IP.

  • DOM injection is skipped outside the browser, so setFont() is SSR-safe.