# Resolve document fonts

> See how each DOCX font resolves, then provide licensed files for missing families.



The theme from the previous page styles controls around the document. Document fonts are separate. A DOCX stores family
names such as Aptos, but may not contain the corresponding font files.

Continue with `/sample.docx` from the [Quickstart](/editor/quickstart). It requests Aptos for body text and Aptos Display
for headings. Start without adding a `fonts` configuration.

## Start with the available fonts [#start-with-the-available-fonts]

By default, SuperDoc keeps each font name from the DOCX. It uses an embedded font when the document contains one and its
embedding permissions allow it. Otherwise, the browser uses an installed system font with that name. With no font
provider configured, SuperDoc does not fetch a replacement for an unavailable document family.

One built-in provider is the exception. SuperDoc always registers a core-symbol face and requests it when the document
contains symbol or dingbat characters it covers — including the `•` bullet in the Quickstart sample. That request
happens with no `fonts` configuration at all, so the package's bundled font assets must stay reachable under your
Content Security Policy even before you add a provider. It supplies those glyphs only; it never substitutes a document
family.

If the requested font is unavailable, the browser paints with its fallback. The text remains editable and export keeps
the original DOCX font name, but different glyph widths can change line and page breaks.

| Your document and audience                             | Recommended path                                                      |
| ------------------------------------------------------ | --------------------------------------------------------------------- |
| The DOCX contains an eligible embedded font            | Open the document without font configuration.                         |
| Every supported device has the requested font          | Use the system font and verify each supported environment.            |
| The font is proprietary or is not installed everywhere | Host licensed web-font files and register each required face.         |
| You intentionally accept a different typeface          | Register that font, map the DOCX family to it, and verify pagination. |

The font names shown in the built-in toolbar are choices, not font files bundled with `superdoc`.

## See the resolution path [#see-the-resolution-path]

Compare what happens to the Aptos body text in the Quickstart document. The DOCX name stays the same while the available
provider changes.

> **Interactive model: how a document font resolves**
>
> | Scenario | DOCX requests | Provider | SuperDoc resolves | DOCX exports | Diagnostic |
> | --- | --- | --- | --- | --- | --- |
> | System font | Aptos | Installed Aptos | Aptos | Aptos | reason: `as_requested`; loadStatus: `unloaded`; systemAvailability: `available`; missing: `false` |
> | Hosted font | Aptos | /fonts/aptos-regular.woff2 | Aptos | Aptos | reason: `registered_face`; loadStatus: `loaded`; missing: `false` |
> | Unavailable font | Aptos | No usable Aptos face | Aptos | Aptos | reason: `as_requested`; loadStatus: `unloaded`; systemAvailability: `unavailable`; missing: `true` |


For an available system font, `loadStatus: 'unloaded'` does not mean the font is missing. It means SuperDoc did not load a
registered font asset. `systemAvailability` reports whether the browser can use the system face.

## Host a proprietary font [#host-a-proprietary-font]

Host a proprietary font only when its license permits web delivery. For the Quickstart document, place the licensed
Aptos files under `public/fonts`, then register the families and faces the document uses. Each tab is the Quickstart
file with `fonts` added, so the export button the fidelity checklist relies on keeps working:

**Vanilla — `src/main.ts`**

```ts
import { SuperDoc, type Config } from 'superdoc';
import 'superdoc/style.css';

const exportButton = document.querySelector<HTMLButtonElement>('#export-docx');

if (!exportButton) throw new Error('The export button is missing.');

const documentFonts = {
  families: [
    {
      family: 'Aptos',
      faces: [
        { source: '/fonts/aptos-regular.woff2', weight: 400, style: 'normal' },
        { source: '/fonts/aptos-bold.woff2', weight: 700, style: 'normal' },
      ],
    },
    {
      family: 'Aptos Display',
      faces: [{ source: '/fonts/aptos-display.woff2', weight: 400, style: 'normal' }],
    },
  ],
} satisfies NonNullable<Config['fonts']>;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/sample.docx',
  fonts: documentFonts,
  onReady: () => {
    exportButton.disabled = false;
  },
  onContentError: ({ error }) => {
    console.error('SuperDoc could not open the document.', error);
  },
  onException: ({ error }) => {
    console.error('SuperDoc could not open the document.', error);
  },
});

exportButton.addEventListener('click', async () => {
  exportButton.disabled = true;
  try {
    await superdoc.export({ exportType: ['docx'], exportedName: 'sample-edited' });
  } catch (error) {
    console.error('SuperDoc could not export the document.', error);
  } finally {
    exportButton.disabled = false;
  }
});

```

**React — `src/App.tsx`**

```tsx
import { useRef, useState } from 'react';
import { SuperDocEditor, type SuperDocConfig, type SuperDocRef } from '@superdoc/react';
import '@superdoc/react/style.css';

const documentFonts = {
  families: [
    {
      family: 'Aptos',
      faces: [
        { source: '/fonts/aptos-regular.woff2', weight: 400, style: 'normal' },
        { source: '/fonts/aptos-bold.woff2', weight: 700, style: 'normal' },
      ],
    },
    {
      family: 'Aptos Display',
      faces: [{ source: '/fonts/aptos-display.woff2', weight: 400, style: 'normal' }],
    },
  ],
} satisfies NonNullable<SuperDocConfig['fonts']>;

export default function App() {
  const editorRef = useRef<SuperDocRef>(null);
  const exportingRef = useRef(false);
  const [ready, setReady] = useState(false);
  const [exporting, setExporting] = useState(false);

  async function exportDocument() {
    if (exportingRef.current) return;
    exportingRef.current = true;
    setExporting(true);
    try {
      await editorRef.current?.getInstance()?.export({ exportType: ['docx'], exportedName: 'sample-edited' });
    } catch (error) {
      console.error('SuperDoc could not export the document.', error);
    } finally {
      exportingRef.current = false;
      setExporting(false);
    }
  }

  return (
    <main>
      <button disabled={!ready || exporting} onClick={() => void exportDocument()} type='button'>
        Export DOCX
      </button>
      <SuperDocEditor
        document='/sample.docx'
        fonts={documentFonts}
        onContentError={({ error }) => console.error('SuperDoc could not open the document.', error)}
        onException={({ error }) => console.error('SuperDoc could not open the document.', error)}
        onReady={() => setReady(true)}
        ref={editorRef}
      />
    </main>
  );
}

```


The `Config['fonts']` type behind `documentFonts` checks the configuration while preserving its specific values. `family`
matches the logical name stored in the DOCX. Because the names match, no mapping is needed. Each `source` identifies one
physical face. Register regular, bold, italic, and bold italic when the document uses them; one face does not make the
others available.

The sample's bullet markers are set in `Symbol` by its numbering definition, so the report also lists a `Symbol` row.
That family ships with most desktop systems and is not one you would normally host; if it reports `missing: true` on a
target device, map it to a face you can provide rather than registering a licensed `Symbol` file.

A font loaded by your application's `@font-face` CSS can also be available to the browser. Prefer `fonts.families` for
document fonts so SuperDoc knows which source and face descriptors to load before it measures the document.

SuperDoc registers these providers at startup and loads required faces before measuring when it can. A missing or blocked
asset still allows the document to open, but the report identifies the fallback.

## Check what rendered [#check-what-rendered]

Subscribe after the Editor is ready. `fonts.onReport()` immediately replays the current report when one exists, then
reports changes such as a late font load. Save this helper as `src/font-report.ts`; it takes a SuperDoc instance, so
both frameworks use the same file:

```ts
import type { SuperDoc } from 'superdoc';

export function observeDocumentFonts(superdoc: SuperDoc) {
  return superdoc.fonts.onReport(({ report = [] }) => {
    for (const font of report) {
      console.log({
        logicalFamily: font.logicalFamily,
        physicalFamily: font.physicalFamily,
        reason: font.reason,
        loadStatus: font.loadStatus,
        systemAvailability: font.systemAvailability,
        exportFamily: font.exportFamily,
        missing: font.missing,
        // Face-level rows repeat a family per weight/style; without this, a failed bold is
        // indistinguishable from the regular that loaded beside it.
        face: font.face,
        // A substitution can load cleanly and still reflow the document. `evidence.lineBreakSafe`
        // is the only field that separates a metric-safe substitute from a visual-only one.
        evidence: font.evidence,
      });
    }
  });
}

```

Import it where you create the Editor and keep the returned function for teardown. These are additions to the complete
files above, not replacements — the export button and error handlers they already set up stay as they are. Vanilla
subscribes from `onReady` and unsubscribes on unload:

```ts
// src/main.ts — additions to the file you already have.
import { observeDocumentFonts } from './font-report';

let stopFontReport: (() => void) | undefined;

// Inside the existing onReady, alongside `exportButton.disabled = false`:
stopFontReport = observeDocumentFonts(superdoc);

// The Quickstart has no teardown listener yet; add one:
window.addEventListener('beforeunload', () => {
  stopFontReport?.();
});
```

React subscribes from the same callback and releases the handle when the component unmounts:

```tsx
// src/App.tsx — additions to the file you already have.
import { useEffect } from 'react';
import { observeDocumentFonts } from './font-report';

const stopFontReport = useRef<(() => void) | undefined>(undefined);

useEffect(() => () => stopFontReport.current?.(), []);

// Inside the existing onReady, alongside `setReady(true)`:
const instance = editorRef.current?.getInstance();
if (instance) stopFontReport.current = observeDocumentFonts(instance);
```

Use these fields to decide whether the document needs another font provider:

| Field                | What it tells you                                                                 |
| -------------------- | --------------------------------------------------------------------------------- |
| `logicalFamily`      | Font family requested by the DOCX.                                                |
| `physicalFamily`     | Family SuperDoc resolved for measurement and paint.                               |
| `reason`             | Why that physical family was selected.                                            |
| `loadStatus`         | State of a registered font asset.                                                 |
| `systemAvailability` | Whether an unregistered system face is available.                                 |
| `exportFamily`       | Font family that export preserves in the DOCX.                                    |
| `missing`            | Whether SuperDoc has confirmed that the requested face lacks a faithful provider. |
| `face`               | Weight and style on face-level rows; absent on family-level rows.                 |

Act on `missing: true`, not on a transient `loadStatus`. A system face with `systemAvailability: 'unknown'` is unresolved,
but SuperDoc does not call it missing without evidence.

When `reason` is `as_requested` and `missing` is `true`, `physicalFamily` remains the requested name. The browser chose a
fallback, but does not report that fallback's family to SuperDoc.

Use `superdoc.fonts.getReport()` when you only need the current snapshot.

## Map an intentional substitute [#map-an-intentional-substitute]

Use a mapping when the font you can provide has a different family name from the one stored in the DOCX. This example
substitutes Inter for both Aptos families, so place `inter-regular.woff2` and `inter-bold.woff2` in `public/fonts`
alongside the Aptos files before continuing — the mapping resolves to a browser fallback if those assets are missing.

Change only the `documentFonts` value in the file you already have: it registers Inter and maps both DOCX families onto
it. Everything else — including the `observeDocumentFonts` wiring from the previous section — stays as it is, because the
fidelity checklist below reads the report that wiring produces. The snippets show the whole file for context.

**Vanilla — `src/main.ts`**

```ts
import { SuperDoc, type Config } from 'superdoc';
import 'superdoc/style.css';

const exportButton = document.querySelector<HTMLButtonElement>('#export-docx');

if (!exportButton) throw new Error('The export button is missing.');

const documentFonts = {
  families: [
    {
      family: 'Inter',
      faces: [
        { source: '/fonts/inter-regular.woff2', weight: 400, style: 'normal' },
        { source: '/fonts/inter-bold.woff2', weight: 700, style: 'normal' },
      ],
    },
  ],
  map: {
    Aptos: 'Inter',
    'Aptos Display': 'Inter',
  },
} satisfies NonNullable<Config['fonts']>;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/sample.docx',
  fonts: documentFonts,
  onReady: () => {
    exportButton.disabled = false;
  },
  onContentError: ({ error }) => {
    console.error('SuperDoc could not open the document.', error);
  },
  onException: ({ error }) => {
    console.error('SuperDoc could not open the document.', error);
  },
});

exportButton.addEventListener('click', async () => {
  exportButton.disabled = true;
  try {
    await superdoc.export({ exportType: ['docx'], exportedName: 'sample-edited' });
  } catch (error) {
    console.error('SuperDoc could not export the document.', error);
  } finally {
    exportButton.disabled = false;
  }
});

```

**React — `src/App.tsx`**

```tsx
import { useRef, useState } from 'react';
import { SuperDocEditor, type SuperDocConfig, type SuperDocRef } from '@superdoc/react';
import '@superdoc/react/style.css';

const documentFonts = {
  families: [
    {
      family: 'Inter',
      faces: [
        { source: '/fonts/inter-regular.woff2', weight: 400, style: 'normal' },
        { source: '/fonts/inter-bold.woff2', weight: 700, style: 'normal' },
      ],
    },
  ],
  map: {
    Aptos: 'Inter',
    'Aptos Display': 'Inter',
  },
} satisfies NonNullable<SuperDocConfig['fonts']>;

export default function App() {
  const editorRef = useRef<SuperDocRef>(null);
  const exportingRef = useRef(false);
  const [ready, setReady] = useState(false);
  const [exporting, setExporting] = useState(false);

  async function exportDocument() {
    if (exportingRef.current) return;
    exportingRef.current = true;
    setExporting(true);
    try {
      await editorRef.current?.getInstance()?.export({ exportType: ['docx'], exportedName: 'sample-edited' });
    } catch (error) {
      console.error('SuperDoc could not export the document.', error);
    } finally {
      exportingRef.current = false;
      setExporting(false);
    }
  }

  return (
    <main>
      <button disabled={!ready || exporting} onClick={() => void exportDocument()} type='button'>
        Export DOCX
      </button>
      <SuperDocEditor
        document='/sample.docx'
        fonts={documentFonts}
        onContentError={({ error }) => console.error('SuperDoc could not open the document.', error)}
        onException={({ error }) => console.error('SuperDoc could not open the document.', error)}
        onReady={() => setReady(true)}
        ref={editorRef}
      />
    </main>
  );
}

```


SuperDoc registers Inter, follows each `map` entry, and loads the required faces before initial measurement. The mapping
changes measurement and paint for this document. It does not rename the fonts in the DOCX; export still preserves Aptos
and Aptos Display.

Use `superdoc.fonts.add()`, `superdoc.fonts.map()`, and `superdoc.fonts.preload()` instead when a user chooses a provider
after the document opens. `preload()` accepts logical DOCX family names and follows the active mapping.

An arbitrary substitute can change layout even when it loads successfully. Prefer the original font or a substitute
whose metrics you have evaluated for the documents you support.

## Verify font fidelity [#verify-font-fidelity]

* Confirm every report row has the expected `physicalFamily`, `missing: false`, **and** positive evidence that the face
  resolved: a registered face at `loadStatus: 'loaded'`, or a pass-through face at `systemAvailability: 'available'`.
  Both halves are needed. `missing: false` alone is not enough, because the report withholds `missing` while
  availability is `checking` or `unknown`; a loaded face alone is not enough either, because a non-metric
  `category_fallback` loads successfully and still reports `missing: true`.
* Rows whose `reason` is `bundled_substitute` or `category_fallback` also carry `evidence`. Check
  `evidence.lineBreakSafe` on those: such a row can report `loadStatus: 'loaded'` and `missing: false` while the runtime
  marks it `verdict: 'visual_only'` with `lineBreakSafe: false` — Cooper Black resolving to Caprasimo is one. Those rows
  render, but their advances do not preserve line breaks, so treat them as a layout change to review rather than a
  passing fidelity check. Every other reason, including the `custom_mapping` this page's Inter mapping produces, leaves
  `evidence` undefined; verify those with the line and page break comparison below.
* Exercise every weight and style your documents use; a loaded regular face does not verify bold or italic.
* Compare line and page breaks in SuperDoc and Word after changing a provider or mapping.
* Export and reopen the DOCX. The original logical family should remain selected.
* For cross-origin assets, allow the font origin in CORS and Content Security Policy rules.

Continue with [Track changes](/editor/track-changes) to let reviewers propose and decide edits without immediately
changing the accepted document.
