---
title: "Fonts — Svelte"
description: "List the fonts a document can be written in, see what each one allows, and draw text in a browser in the font the engine writes it with."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/documents/fonts"
---

# Fonts

Text the engine writes, in a free text box, a form field or a redaction label, is in the font its
`fontFamily` names. Register `fontsPlugin()` to list the fonts a document can be written in, and to
draw text in a browser in the very font the engine will write it with:

**`font-select.svelte`**

```svelte
<script lang="ts">
  import { useFontsState } from '@embedpdf/svelte/fonts';

  let { value = $bindable() }: { value: string } = $props();
  const fontsState = useFontsState();
  const choosable = $derived(fontsState.fonts?.fonts.filter((font) => font.canChoose) ?? []);
</script>

<select bind:value>
  {#each choosable as font (font.name)}
    <option value={font.name}>{font.name}</option>
  {/each}
</select>
```

The annotation and form layers use the plugin by themselves: with it, text being typed shows in
the fonts the engine writes it with; without it, in the standard font the engine falls back to.
Nothing is read when a document opens: the fonts are read the first time something asks for them.

## The fonts you can use

`fonts` has
every font, with where it comes from:

| `source`     | The fonts                                                                          |
| :----------- | :--------------------------------------------------------------------------------- |
| `'standard'` | The 14 standard PDF fonts, drawn the same everywhere. They only cover Latin text.  |
| `'engine'`   | The engine's own fonts: the ones you register locally, your server's on the cloud. |
| `'document'` | The fonts the PDF already has.                                                     |

A font's `name` is what `fontFamily` takes. `canChoose` says whether new text can be written in it,
and when it can't, `restrictions` says why:

| Restriction            | Why                                                                        |
| :--------------------- | :------------------------------------------------------------------------- |
| `'subset'`             | The PDF holds only the characters its text uses.                           |
| `'not-embedded'`       | The PDF only names the font.                                               |
| `'preview-and-print'`  | Its licence allows showing and printing, not editing.                      |
| `'unsupported-format'` | A Type 1, CFF or Type 3 font: new text is written in TrueType or OpenType. |
| `'no-unicode-map'`     | The font doesn't say which character each of its letters is.               |
| `'not-served'`         | A server font whose file stays on the server. It can still be chosen.      |

A box keeps the font it has either way. Every field of a font is on the engine's
[Fonts](https://www.embedpdf.com/docs/engine/documents/fonts#the-fonts-you-can-use) page.

The list keeps up by itself. A write that puts a font in the document, such as Hebrew typed in a
box, which the engine fills in from a Hebrew font, reads it again:

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { annotationPlugin } from '@embedpdf/svelte/annotation';
  import { fontsPlugin } from '@embedpdf/svelte/fonts';
  import { localEngine } from '@embedpdf/engine';
  import Fonts from './Fonts.svelte';

  import '../list.css';

  const engine = localEngine();
  const plugins = [interactionPlugin(), annotationPlugin(), fontsPlugin()];

  const welcome = async (): Promise<OpenInput> => {
    const response = await fetch('/pdfs/welcome.pdf');
    return { kind: 'bytes', id: 'welcome', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: welcome }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Fonts />
  </DocumentGate>
</Viewer>
```

**`list.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --accent: #0b6bcb;
  --ready: #1f8a4c;
  --ready-tint: #e8f6ee;
  --waiting: #a86a00;
  --waiting-tint: #fdf3e1;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The fonts, in a card */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
  overflow: hidden;
}

.panel-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--canvas);
  border-bottom: 1px solid var(--line);
}

.panel-title {
  margin: 0 auto 0 0;
  font-size: 14px;
  font-weight: 650;
  color: var(--ink);
}

.button {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--accent);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 5px 10px;
  cursor: pointer;
}

.button:hover {
  border-color: var(--accent);
}

/* The load state: loading, ready, forbidden or error */
.status {
  font-size: 11.5px;
  font-weight: 600;
  border-radius: 999px;
  padding: 3px 9px;
  color: var(--waiting);
  background: var(--waiting-tint);
}

.status[data-status='ready'] {
  color: var(--ready);
  background: var(--ready-tint);
}

/* A group per source, its fonts as chips */
.group {
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
}

.group:last-child {
  border-bottom: 0;
}

.group-title {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 650;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.chip {
  display: flex;
  flex-direction: column;
  gap: 2px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 5px 9px;
  font-size: 12.5px;
  color: var(--ink);
}

/* A font new text can't be written in: greyed, with why */
.chip[data-choosable='false'] {
  color: var(--muted);
  background: var(--canvas);
}

.chip-why {
  font-size: 11px;
}

.empty,
.loading {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

.loading {
  padding: 24px 0;
  text-align: center;
}
```

**`Fonts.svelte`**

```svelte
<!-- Every font the document can be written in, grouped by where it comes from. -->
<script lang="ts">
  import { usePageList, type Font, type FontRestriction } from '@embedpdf/svelte/runtime';
  import { useAnnotation } from '@embedpdf/svelte/annotation';
  import { useFontsState } from '@embedpdf/svelte/fonts';

  const GROUPS = [
    ['document', 'In this document'],
    ['engine', 'The engine’s fonts'],
    ['standard', 'Standard'],
  ] as const;

  // Why a font can't be chosen for new text, by its first restriction.
  const WHY: Record<FontRestriction, string> = {
    subset: 'holds only the characters its text uses',
    'not-embedded': 'the file only names it',
    'preview-and-print': 'licensed for viewing and printing only',
    'unsupported-format': 'new text is written in TrueType or OpenType',
    'no-unicode-map': 'doesn’t say which characters it draws',
    'not-served': 'stays on the server',
  };

  const fontsState = useFontsState();
  const annotation = useAnnotation();
  const pages = usePageList();

  const whyOf = (font: Font) =>
    font.canChoose ? null : WHY[font.restrictions[0] ?? 'unsupported-format'];

  // The engine fills Hebrew in from a Hebrew font and puts it in the file: the list reads it.
  async function writeHebrew() {
    const cover = pages.current[0]?.ref;
    if (!cover) return;
    await annotation.create(cover, {
      subtype: 'free-text',
      box: { x: 72, y: 72, width: 220, height: 40 },
      contents: 'שלום עולם',
      fontFamily: 'helvetica',
    });
  }
</script>

<section class="panel">
  <header class="panel-header">
    <h3 class="panel-title">Fonts</h3>
    <button type="button" class="button" onclick={writeHebrew}>Write in Hebrew</button>
    <span class="status" data-status={fontsState.status}>{fontsState.status}</span>
  </header>
  {#each GROUPS as [source, title] (source)}
    {@const group = fontsState.fonts?.fonts.filter((font) => font.source === source) ?? []}
    <div class="group">
      <h4 class="group-title">{title}</h4>
      {#if group.length === 0}
        <p class="empty">None</p>
      {:else}
        <ul class="chips">
          {#each group as font (font.name)}
            {@const why = whyOf(font)}
            <li class="chip" data-choosable={font.canChoose}>
              <span class="chip-name">{font.name}</span>
              {#if why}<span class="chip-why">{why}</span>{/if}
            </li>
          {/each}
        </ul>
      {/if}
    </div>
  {/each}
</section>
```

## Draw text in a font

`useFontCss(font)`
gives the CSS a font is drawn with, and mounts its programs (the font files) while your component
lives:

**`font-preview.svelte`**

```svelte
<script lang="ts">
  import { useFontCss } from '@embedpdf/svelte/fonts';

  let { font }: { font: string } = $props();
  // Its programs are mounted while this component lives.
  const css = useFontCss(() => font);
</script>

<input
  value="Hello שלום"
  style:font-family={css.current?.fontFamily}
  style:font-weight={css.current?.fontWeight}
  style:font-style={css.current?.fontStyle}
  style:font-synthesis="none"
/>
```

Draw with `font-synthesis: none`, so the browser never fakes a bold or an italic the engine won't
draw. The CSS names the engine's own programs, then the same fallback fonts the engine fills
missing characters from, in its order, then the standard font that stands in for the font (so
Latin a font lacks shows in Helvetica), then a font that draws a character no font has as a box,
as the engine writes it. Until the browser has every program, it names the system's closest fonts
instead, and the CSS changes when they arrive.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { fontsPlugin } from '@embedpdf/svelte/fonts';
  import { localEngine } from '@embedpdf/engine';
  import FontPreview from './FontPreview.svelte';

  import '../draw.css';

  const engine = localEngine();
  const plugins = [fontsPlugin()];

  const welcome = async (): Promise<OpenInput> => {
    const response = await fetch('/pdfs/welcome.pdf');
    return { kind: 'bytes', id: 'welcome', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: welcome }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <FontPreview />
  </DocumentGate>
</Viewer>
```

**`draw.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The font picker and the text drawn in it, in a card */
.panel {
  display: grid;
  gap: 12px;
  padding: 16px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.field {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}

.field select {
  min-width: 0;
  flex: 1;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 8px;
  font: inherit;
  font-weight: 400;
  color: var(--ink);
  background: var(--surface);
}

/* The text, in the chosen font */
.preview {
  min-height: 96px;
  resize: vertical;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px;
  font-size: 24px;
  line-height: 1.4;
  color: var(--ink);
  background: var(--canvas);
}

.hint,
.loading {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
}

.loading {
  padding: 24px 0;
  text-align: center;
}
```

**`FontPreview.svelte`**

```svelte
<!-- Type in any font the document can be written in, drawn as the engine will write it. -->
<script lang="ts">
  import { useFontCss, useFontsState } from '@embedpdf/svelte/fonts';

  const fontsState = useFontsState();
  let font = $state('times-roman');
  let text = $state('Hello שלום مرحبا 你好');
  // The font's CSS, its programs mounted while this component lives.
  const css = useFontCss(() => font);
  const choosable = $derived(fontsState.fonts?.fonts.filter((entry) => entry.canChoose) ?? []);
</script>

<section class="panel">
  <label class="field">
    Font
    <select bind:value={font}>
      {#each choosable as entry (entry.name)}
        <option value={entry.name}>{entry.name}</option>
      {/each}
    </select>
  </label>
  <textarea
    class="preview"
    style:font-family={css.current?.fontFamily}
    style:font-weight={css.current?.fontWeight}
    style:font-style={css.current?.fontStyle}
    style:font-synthesis="none"
    bind:value={text}
  ></textarea>
  <p class="hint">Characters no font has show as boxes, as the engine writes them.</p>
</section>
```

## Your own editor

To lay text out yourself, ask what the engine will do. `resolve()` says which font a write will
draw each run of text in; `getRunCss(run)` gives the CSS for a run as it answered.
`getPaintFonts(font, text)` gives the fonts themselves, measured as the browser draws them: what
`@embedpdf/core-text` lays text out with, so it breaks lines where the engine will. See
[Show typed text in a browser](https://www.embedpdf.com/docs/engine/documents/fonts#show-typed-text-in-a-browser) for what
each answer holds.

## Methods

Everything `useFonts()` gives you.

| Method                          | What it does                                                                                                     |
| :------------------------------ | :--------------------------------------------------------------------------------------------------------------- |
| `ensureLoaded()`                | Reads the document's fonts unless they're loaded or loading. Resolves once they're there.                        |
| `refresh()`                     | Reads them again. Rarely needed: the list keeps up by itself.                                                    |
| `resolve(request)`              | The fonts a write would draw each run in, without writing. [Your own editor](#your-own-editor)                   |
| `getProgram(displayHash)`       | A font file's facts: the characters it covers and its line metrics.                                              |
| `getFontCss(font)`              | The CSS text in a font is drawn with, and the programs it names. [Draw text in a font](#draw-text-in-a-font)     |
| `getFaceCss(face)`              | The same for a face, as rich text names one: `{ family, weight, italic }`.                                       |
| `getRunCss(run)`                | The same for a run as `resolve()` answered it.                                                                   |
| `getPaintFonts(font, text)`     | The fonts a box in `font` draws `text` in, measured; `null` while they load. [Your own editor](#your-own-editor) |
| `getPaintFontsEpoch()`          | A number that moves when any of the answers above may change: ask again when it does.                            |
| `getFaceOfCssFamily(cssFamily)` | The face a program's CSS family stands for, to read back text the browser drew itself.                           |

`resolve()` and `getProgram()` also take a `signal` in their options, to cancel them.

## State

Everything `useFontsState()` gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.

| Field    | What it is                                                                                                                                      | Read once       |
| :------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- |
| `fonts`  | Every font the document can be written in, as listed under [The fonts you can use](#the-fonts-you-can-use), or `null` until the list has loaded | `getFontList()` |
| `status` | `'idle'` until something asks for the list, then `'loading'`, `'ready'`, `'forbidden'` or `'error'`                                             | `getStatus()`   |



## Events

| Event               | Fires when                                                                                         | Carries |
| :------------------ | :------------------------------------------------------------------------------------------------- | :------ |
| `onFontListChanged` | The list was loaded, or changed: a write put a font in the document, or the engine's fonts changed | `fonts` |

## Good to know

### Fonts that stay on the engine

A font whose file can't go to a browser, one the PDF only names or a server font kept on the
server, is drawn in the standard font the engine falls back to for it while text is typed. The
engine's own picture of the box follows the write.

### On the cloud

Downloading a font file needs a token that may write text in the document. With a read-only token
the list is still there; the standard fonts, which come with the viewer, show as they are, and
any other font in the system's closest font.
