Fonts and measurement
Supply font bytes for accurate DOCX line wrapping and pagination with embedded fonts, packaged substitutes, or your own files.
The editor shapes text with HarfBuzz when it has usable font bytes. Without them, it uses fallback measurement: canvas in a browser when available, fixed measurement elsewhere. The document still opens, but line and page breaks can differ from the original.
CSS fonts and font picker entries do not supply font bytes. Only a configured font source makes measurement accurate.
Configure document fonts
Pass resolvers to useDocxSource. Vue provides the same API.
import { useDocxSource } from '@docx-editor.dev/react';
import { packagedFonts } from '@docx-editor.dev/fonts';
import { googleFonts } from '@docx-editor.dev/fonts/google';
const { document, fonts } = useDocxSource(url, {
fonts: [packagedFonts(), googleFonts()],
});The list is a precedence order. The first source that supplies a face wins. No optional font source is enabled by default.
To pass fonts to the fonts prop directly, wrap them in useFonts. An inline resolver is a different function on each render. Each identity change calls setFonts() and resolves the fonts again. The editor keeps the document, selection, and undo history.
import { packagedFonts } from '@docx-editor.dev/fonts';
import { googleFonts } from '@docx-editor.dev/fonts/google';
import { DocxEditor, useFonts } from '@docx-editor.dev/react';
function Editor({ bytes }: { bytes: Uint8Array }) {
const fonts = useFonts(packagedFonts(), googleFonts());
return <DocxEditor document={bytes} fonts={fonts} />;
}<script setup lang="ts">
import { packagedFonts } from '@docx-editor.dev/fonts';
import { googleFonts } from '@docx-editor.dev/fonts/google';
import {
DocxEditorContent,
DocxEditorRoot,
DocxEditorViewport,
useFonts,
} from '@docx-editor.dev/vue';
defineProps<{ bytes: Uint8Array }>();
const fonts = useFonts(packagedFonts(), googleFonts());
</script>
<template>
<DocxEditorRoot :document="bytes" :fonts="fonts">
<DocxEditorViewport>
<DocxEditorContent />
</DocxEditorViewport>
</DocxEditorRoot>
</template>Each useFonts argument is a resolver, a fragment, a configuration, or a promise for one. Arguments compose in order, and the first wins.
In Vue, a function argument is always the value, never a getter. Use computed(() => packagedFonts()) to build one lazily.
Choose a font source
| Source | Third-party requests | Loads |
|---|---|---|
| Fonts embedded in the DOCX | No | The faces in the file, automatically |
packagedFonts() | No | The packaged families the document uses, plus its default face |
defaultFonts() | No | All 20 faces of the five defaults, before the document opens |
googleFonts() | Yes, for cataloged families | The catalog families the document uses, plus its default face |
customFonts() | Depends on your URLs | All configured faces when the editor resolves fonts |
loadFonts() | Depends on your URLs | The URLs you list, immediately |
| No usable source | No | Nothing; fallback measurement |
@docx-editor.dev/fonts ships these open-licensed, metric-compatible substitutes:
| Original font | Substitute | License | In defaultFonts() |
|---|---|---|---|
| Calibri | Carlito | SIL OFL | Yes |
| Cambria | Caladea | SIL OFL | Yes |
| Times New Roman | Liberation Serif | SIL OFL | Yes |
| Arial | Liberation Sans | SIL OFL | Yes |
| Courier New | Liberation Mono | SIL OFL | Yes |
| Century Gothic | TeX Gyre Adventor | GUST Font License | No |
| Segoe UI | Selawik | SIL OFL | No |
packagedFonts()covers all seven families. It also serves Helvetica with the Liberation Sans faces.- To load all seven eagerly, call
defaultFonts({ families: ALL_DEFAULT_FONT_FAMILIES }). The two extra families add about 797 KB. - Narrow
packagedFonts()andgoogleFonts()withallow, for examplepackagedFonts({ allow: ['Calibri'] }). - Substitutes match advance widths, but kerning can still change line breaks. Supply licensed bytes when pixels must match.
- Importing the package fetches nothing. Font binaries load as separate assets.
The preferred names DefaultFontFamily, DEFAULT_FONT_FAMILIES, and ALL_DEFAULT_FONT_FAMILIES replace the deprecated WordDefaultFamily, WORD_DOCUMENT_DEFAULT_FAMILIES, and ALL_WORD_DEFAULT_FAMILIES. The deprecated aliases still work.
Choose between lazy and eager loading
packagedFonts() and defaultFonts() measure the same for the families they share.
| Behavior | packagedFonts() | defaultFonts() |
|---|---|---|
| Runs | After the document is parsed | Before the document opens |
| Covers | All seven packaged families | The five common DOCX default fonts |
| Loads | The families in the document, plus the default face | All 20 faces, 7.4 MB |
| First layout | Fallback measurement, then re-paginates | Correct on the first pass |
| Undo history across the swap | Kept | Kept |
Pass defaultFonts when a visible re-pagination is worse than the extra bytes. useDocxSource then waits for fonts before it returns the document.
import { defaultFonts } from '@docx-editor.dev/fonts';
const { document, fonts } = useDocxSource(url, { fonts: defaultFonts });Load your own fonts
Use customFonts() for brand or licensed fonts. Put it first so its faces take precedence.
import { customFonts } from '@docx-editor.dev/core/editor';
import { packagedFonts } from '@docx-editor.dev/fonts';
import { DocxEditor, useFonts } from '@docx-editor.dev/react';
function Editor({ bytes }: { bytes: Uint8Array }) {
const fonts = useFonts(
customFonts({
sources: [
{ url: '/fonts/AcmeSans-Regular.ttf', family: 'Acme Sans', weight: 400, style: 'normal' },
{ url: '/fonts/AcmeSans-Bold.ttf', family: 'Acme Sans', weight: 700, style: 'normal' },
],
onFailure: (failure) => console.warn(failure.request.family, failure.reason),
}),
packagedFonts()
);
return <DocxEditor document={bytes} fonts={fonts} />;
}Creating the resolver fetches nothing. onFailure defaults to console.warn, and cancellation does not call it. Loaded fonts appear in the font picker.
Load font bytes eagerly
Call loadFonts() when you need validated bytes before you open a document.
import { createDocxEditor, loadFonts, type FontUrlSource } from '@docx-editor.dev/core/editor';
const sources: FontUrlSource[] = [
{ url: '/fonts/AcmeSans-Regular.ttf', family: 'Acme Sans', weight: 400, style: 'normal' },
];
const editor = createDocxEditor({ document: bytes, fonts: await loadFonts({ sources }) });
editor.attach(container);loadFonts case | Result |
|---|---|
| One source fails | Returns the valid sources and a typed failures list |
| Hash matches | Accepts the source |
| Hash differs | Rejects the source with hashMismatch |
| Hash omitted | Computes a hash for the returned source |
| Same request repeats | Uses the Cache API when available. Without it, each call fetches again. |
Pin every URL that you do not control. Load once without a hash, then store each returned source.hash.
If you already have bytes, call createFontSource. It returns a typed failure instead of throwing.
import { composeFontConfiguration, createFontSource } from '@docx-editor.dev/core/editor';
const made = createFontSource(bytes, { family: 'Acme Sans', weight: 400, style: 'normal' });
const fonts = 'source' in made ? composeFontConfiguration({ sources: [made.source] }) : undefined;Write your own resolver
Wrap a resolver in defineFontResolver. useDocxSource uses the marker to tell a resolver from a loader such as defaultFonts. useFonts accepts an unmarked resolver only as its first argument.
import { defineFontResolver } from '@docx-editor.dev/core/editor';
const brandFonts = defineFontResolver(async ({ families, defaultFamily, resolvedFaces }) => {
// Match names against a closed set. Skip faces in resolvedFaces.
const wanted = [defaultFamily, ...families].filter((family) => BRAND.has(family));
return { sources: await loadBrandFaces(wanted) };
});| Request field | Contents |
|---|---|
families | Families the document declares, including w:sym, SYMBOL field, and numbering marker faces |
defaultFamily | The face for runs that name no font |
resolvedFaces | Faces that earlier sources already supplied |
A resolver can also return supportedFamilies to add catalog entries to the font picker without bytes.
Control loading and caching
Resolvers run when the editor resolves fonts and again when an edit adds a family that is not loaded. Font arrivals keep the selection, pending formatting, and undo history.
| Rule | Behavior |
|---|---|
| Precedence | An explicit source beats an embedded face. An embedded face beats a substitution. The first fragment wins between equal sources. |
| Duplicates | Composition removes duplicate faces. |
| Retry | The editor requests each family once per load. Reload the document to retry a failed request. |
| Updates | Later family requests read current arguments. Reload to replace loaded faces. |
| Family list | The editor caps the family list passed to a resolver. |
composeFontConfiguration(base, ...fragments) builds one immutable configuration with the same precedence.
Configure Google Fonts
googleFonts() uses a catalog pinned to immutable google/fonts commits with checked sha256: hashes. Pass substitute to redirect families.
googleFonts({
allow: ['Tinos', 'Carlito'],
substitute: { Georgia: 'Tinos' },
fetcher: (input) => fetch(rewriteToYourMirror(String(input))),
onFailure: ({ family, url, diagnostic }) => report(family, url, diagnostic),
});| Option | Description |
|---|---|
allow | Limits the families the resolver serves |
substitute | Redirects a family. Your entries win over the built-in metric map. |
fetcher | Replaces fetch, for a mirror or a persistent cache. packagedFonts() and defaultFonts() accept it too. |
onFailure | Receives each dropped face. Defaults to a console warning. |
- The engine rejects downloaded bytes that do not match their hash.
googleFonts()caches requests in memory per fetcher. The cache does not persist to disk or cross worker boundaries.- Packaged loaders read the requested assets on each call. Supply
fetcherfor a persistent or shared cache. - Century Gothic and Segoe UI come from packaged bytes, with no third-party request.
- An arbitrary substitute can change pagination. Prefer a metric-compatible one.
- Without network access, a dropped face falls back and page breaks move. Use a mirror through
fetcher, or usepackagedFonts()alone.
Handle font failures
Fonts never block a document from opening. Each failed face falls back on its own.
| Host | Notification |
|---|---|
| React root | onFontError |
| Vue root | @font-error |
createDocxEditor | onFontError option |
EditorFontError code | Cause |
|---|---|
missing | No bytes are available for the font |
malformed | The bytes are not a valid font |
overLimit | The font exceeds the font byte budget |
hashMismatch | The bytes do not match the pinned hash |
wasmUnavailable | The HarfBuzz WASM asset did not load |
These are the common codes. For the full list, see the EditorFontErrorCode type.
Read editor.fontMeasurement() to inspect measurement.
| Field | Meaning |
|---|---|
measurer | 'shaped' or 'fixed' |
resolving | true while font resolution runs |
producer | Optional source identifier |
{ measurer: 'fixed', resolving: false } means no usable font source remains.
Deploy font assets
Serve the HarfBuzz WASM asset
Webpack, Turbopack, and Vite emit harfbuzz.wasm automatically. Some esbuild, Bun, and library builds do not. Without it, the editor reports wasmUnavailable and uses fixed measurement.
Copy the file from the installed core package after each upgrade:
import { copyFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
await copyFile(
fileURLToPath(import.meta.resolve('@docx-editor.dev/core/harfbuzz.wasm')),
'public/static/harfbuzz.wasm'
);Call setHarfBuzzWasmUrl before you create the first editor:
import { setHarfBuzzWasmUrl } from '@docx-editor.dev/core/layout';
setHarfBuzzWasmUrl('/static/harfbuzz.wasm');- The shaper reads the URL once per module instance. A later call warns and does nothing.
- A Web Worker has its own module instance and needs its own call.
- Use a URL that your application controls. Never build it from user input or remote configuration.
- Cross-origin hosting needs the origin in
connect-src. WebAssembly needswasm-unsafe-evalinscript-src. - The shaper rejects a stale or mismatched binary. An
overridesentry forharfbuzzjshas no effect; upgrade@docx-editor.dev/core.
Relocate packaged fonts in Node or Bun
For a single-file executable, copy the fonts package's assets/ files into one directory. Set DOCX_EDITOR_FONT_ASSET_ROOT before the package is imported:
DOCX_EDITOR_FONT_ASSET_ROOT=/opt/my-app/fonts ./my-appUse an absolute path or a file: URL, and keep the original file names. The package ignores relative paths, filesystem roots, and other URL schemes, and it reads the setting when its module loads. The setting only moves assets; it does not enable a font source.
Upgrade from page-wide registration
Since 2.18.0, font loaders supply bytes for private editor registration only. Your app's own text keeps its fonts.
- The
packagedFontsoptioninstallis ignored, includingtrue. Remove it. installDefaultFontFaces()does nothing and resolves to0. PasspackagedFonts()ordefaultFonts()through thefontsoption instead.- If app text relied on page-wide fonts, load them with your own CSS.
- fonts: packagedFonts({ install: true, onFailure })
+ fonts: packagedFonts({ onFailure })Limits
- Never build a font URL from a document family name. Match file-supplied names against a closed
Mapor catalog. - A remote font host can learn which families a document uses.
- Embedded fonts register under private aliases. A document cannot replace a page-wide font family in your app.
- Packaged faces do not cover every script. For example, Liberation Sans has no Arabic glyphs. Native fallback renders missing glyphs.
- The font notice lists rendered families with no compatible face. It excludes metric-compatible substitutions.
- A custom
TextMeasurercan implementinkBounds(text, style)for CJK punctuation fitting. Return{ left, right }: conservative horizontal ink bounds for one grapheme, in points from its origin. Include font size and horizontal scale, not trailing character spacing. Returnundefinedwhen bounds are unknown. To measure, use left-aligned text, negateactualBoundingBoxLeft, and convert pixels to points before you apply horizontal scale. - Font slot selection, font name parsing, and theme font defaults follow DOCX feature support.