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
npm install @atlowchemi/webfont-browser-generatorpnpm add @atlowchemi/webfont-browser-generatoryarn add @atlowchemi/webfont-browser-generatorbun add @atlowchemi/webfont-browser-generatorHow 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.
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.
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>');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');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.
| Method | Result | Behavior |
|---|---|---|
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() | void | Terminate 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.
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:
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. SetformatOptions.ttf.tsfor reproducible output. exportCache()reflects incremental updates and resolvesundefinedwhen 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:
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:
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.