Skip to content

webfont-browser-generator logo

Browser API ​

@atlowchemi/webfont-browser-generator wraps the Rust engine with wasm-bindgen and runs generation inside dedicated module workers. The package ships compiled JavaScript, types, worker code and WASM; applications need no Rust tools or install-time compilation.

Try it

The Playground runs this package in your browser: drop in SVGs, adjust the options, and download the fonts.

Installation ​

sh
npm install @atlowchemi/webfont-browser-generator
sh
pnpm add @atlowchemi/webfont-browser-generator
sh
yarn add @atlowchemi/webfont-browser-generator
sh
bun add @atlowchemi/webfont-browser-generator

How to use ​

Import generateWebfonts in your Vite application's client code. Vite automatically bundles the worker and WASM assets; no additional Vite configuration is needed.

src/main.ts
ts
import { generateWebfonts } from '@atlowchemi/webfont-browser-generator';

using result = await generateWebfonts({
    sources: [{ id: 'square', name: 'square', svg: '<svg viewBox="0 0 24 24"><path d="M2 2H22V22H2Z"/></svg>' }],
    types: ['woff2'],
    incremental: true,
    // Pin the font's creation timestamp so the same input always produces the same bytes.
    // Without it, the worker records the current browser time.
    formatOptions: { ttf: { ts: 0 } },
});
const bytes = await result.font('woff2');
const css = await result.generateCss();

Options follow the Rust SVG text API in camelCase, including variants, color selection, missing-glyph policies, format options and inline templates. Unknown fields, including file input/output options, are rejected. There are no JavaScript callbacks. Variant families support TTF, WOFF and WOFF2; ordinary fonts additionally support SVG and EOT.

Without formatOptions.ttf.ts, generation captures the browser clock once and retains that timestamp during updates; fonts reused from a persistent cache keep the timestamp of the session that created them. A fixed Unix timestamp produces reproducible font metadata. WOFF uses the pure-Rust zlib-rs compression backend on both native and wasm32 targets. WOFF2 uses single-threaded Brotli at every supported compression quality (0–11).

Using the output ​

The result holds plain bytes and text, so you can use the font on the current page, offer it as a download, or upload it.

ts
const fontUrl = URL.createObjectURL(new Blob([bytes!], { type: 'font/woff2' }));
// Point the generated CSS at the in-memory font, then apply it.
const style = document.createElement('style');
style.textContent = await result.generateCss();
style.textContent = style.textContent.replace(/url\("?[^)"]*\.woff2[^)"]*"?\)/g, `url("${fontUrl}")`);
document.head.append(style);

document.body.insertAdjacentHTML('beforeend', '<i class="icon icon-square"></i>');
ts
function download(name: string, data: BlobPart, type: string) {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(new Blob([data], { type }));
    link.download = name;
    link.click();
    URL.revokeObjectURL(link.href);
}

download('iconfont.woff2', bytes!, 'font/woff2');
download('iconfont.css', await result.generateCss(), 'text/css');
ts
const outputs = await result.collectOutputs();
const form = new FormData();
for (const [kind, data] of Object.entries(outputs)) {
    form.append(kind, new Blob([data]), `iconfont.${kind}`);
}
await fetch('/api/icon-fonts', { method: 'POST', body: form });

To pass explicit font URLs instead of rewriting the CSS, set cssFontsUrl to the URL prefix where you will serve the files.

Result methods ​

TIP

An explicit dispose() method is available for manually managed lifetimes instead of using Explicit Resource Management. It is idempotent: calling it more than once has no additional effect and does not throw.

MethodResultBehavior
font(format)Promise<Uint8Array | undefined>Copy a requested font; does not render templates
generateCss()Promise<string>Lazy CSS render, even if CSS collection is disabled
generateHtml()Promise<string>Lazy HTML render, reusing CSS
generateTs()Promise<string>Fixed TypeScript metadata, even if TS or CSS collection is disabled
collectOutputs()Promise<CollectedOutputs>Object keyed by output kind, with independently owned bytes; unrequested outputs are omitted
regenerate(membership, changes)Promise<void>Apply source updates; requires incremental: true
add(source, variant?)Promise<void>Append a new source; families require a target variant. Requires incremental: true.
update(id, svg, name?)Promise<void>Replace an existing source, preserving its position. Requires incremental: true.
remove(id)Promise<void>Remove an ID from all designs without emptying a design. Requires incremental: true.
exportCache()Promise<Uint8Array | undefined>Persistent cache bytes to store; undefined when caching is off or nothing changed. See caching
dispose()voidTerminate the worker and reject pending calls. Idempotent: repeated calls have no additional effect and do not throw

CollectedOutputs is Partial<Record<FontType | 'css' | 'html' | 'ts', Uint8Array<ArrayBuffer>>>. Access outputs directly by kind, for example (await result.collectOutputs()).woff2.

Calls on one result execute in submission order. Returned byte arrays remain valid after updates and disposal and cannot mutate the engine's retained buffers. CSS and HTML are compiled lazily; an invalid HTML template does not prevent accessing a font or rendering CSS.

TypeScript metadata ​

await result.generateTs() returns fixed TypeScript source, even when ts or css is false. Set ts: true in generation options to also include UTF-8 bytes under ts in collectOutputs(); collection defaults to off.

ts
export const iconBaseSelector = ".icon" as const;
export const iconClassPrefix = "icon-" as const;
export const icons = [
  "square"
] as const;

export type IconName = (typeof icons)[number];

The readonly tuple follows the engine's canonical icon order and variant-family names. Selectors use the effective templateOptions.baseSelector and templateOptions.classPrefix. Custom CSS may emit different classes: this metadata describes the context, not parsed CSS. There is no TS template, override, callback or browser filesystem destination.

Source additions, removals, renames and batch regeneration refresh the metadata. To change options, create a new result.

Persistent cache ​

Pass cache to reuse work from an earlier session. Start with an empty array, store what exportCache() returns, and pass it back next time. Unchanged icons skip parsing and path processing, and unchanged formats are reused; output is byte-identical.

The package only accepts and returns the bytes (a Uint8Array); storing them is up to your app. Keep them anywhere that holds binary data: IndexedDB (stores Uint8Array natively), the Origin Private File System, Cache Storage, or your own server. Expect several MB for hundreds of icons. In the example, loadCache and saveCache stand for your own storage code:

ts
import { generateWebfonts } from '@atlowchemi/webfont-browser-generator';
// Your own module, not part of the package: for example an IndexedDB wrapper.
import { loadCache, saveCache } from './icon-cache';

const stored = await loadCache('icons'); // Uint8Array | undefined
using result = await generateWebfonts({
    sources,
    types: ['woff2'],
    cache: stored ?? new Uint8Array(),
});
const updated = await result.exportCache();
if (updated) await saveCache('icons', updated);
  • Without a fixed formatOptions.ttf.ts, reused fonts keep the timestamp of the session that created them, so output matches that session rather than the current clock. Set formatOptions.ttf.ts for reproducible output.
  • exportCache() reflects incremental updates and resolves undefined when nothing changed since the cache was loaded or last exported, so you can skip writes.
  • Bytes from another engine version or other settings are ignored, and corrupted or truncated bytes fall back to a correct uncached run. Saved fonts and saved per-icon work carry separate checksums; sessions whose fonts all come from the cache don't read the per-icon work, so damage limited to it is detected (and a fresh cache exported) by the next session that needs it.

Only pass trusted cache bytes

Cached fonts are reused as-is, so only pass bytes your own app stored. Don't accept cache bytes from users or other origins, and clear the stored copy if in doubt.

Incremental updates ​

With incremental: true, single actions derive membership from the worker's current result:

ts
await result.add({ id: 'search', name: 'search', svg: searchSvg });
await result.update('search', replacementSvg, 'renamed');
await result.remove('search');
// For a family, additions require a target variant:
await family.add({ id: 'bold:search', name: 'search', svg: searchSvg }, 'bold');

add(source, variant?) appends a new ID. update(id, svg, name?) preserves its position and, when omitted, its name. remove(id) preserves remaining order. Updates and removals apply to every design consuming the ID. Duplicate additions, unknown IDs, incorrect target variants, and removals that empty a design reject without replacing the result. Calls execute in submission order, including after recoverable failures. None of these methods requires resending unchanged SVGs.

For batches, explicit ordering, or membership reassignment, use regenerate:

ts
await result.regenerate({ sources: ['square'] }, [{ type: 'changed', id: 'square', svg: replacementSvg }]);

Supply complete final IDs in order, with only changed source content. For variants, membership is { variants: [{ name: 'light', ids: [...] }, { name: 'bold', ids: [...] }] } in original design order. Changes use type: 'added' with id/name/svg, type: 'changed' with id/svg and optional name, or type: 'removed' with id. Options and variant configuration stay fixed.

Ordinary validation/build failures reject the call and preserve the existing result for retry. Worker failures or WASM traps invalidate the session and reject pending calls. Create a new result after a fatal failure. dispose() is idempotent; all subsequent calls reject.

Worker lifetime and hosting ​

generateWebfonts(options, { workerUrl }) accepts an optional string or URL for a separately hosted worker. By default, bundlers resolve the worker relative to the package entry.

For separately hosted workers, serve worker.js and webfont_browser_bg.wasm together from the same origin.

Each result has its own worker and WASM instance. Keep a result for incremental work, and dispose it when finished. The package needs neither SharedArrayBuffer nor cross-origin isolation. Vite emits the worker and WASM assets. Serve WASM with application/wasm, and permit worker execution and WASM compilation in your Content Security Policy.

Released under the MIT License.