# web fonts — shipping a typeface with your app

> Naming a font in your theme asks for it; `osy font add` ships it. The command pins a font file in your project's lock, every compile carries it to the server, and the app's stylesheet declares the matching `@font-face` — so the typeface renders on a machine that has never seen it, instead of falling through to the next name in your stack.

<!-- id: ui-web-fonts · area: ui · stability: stable · html: https://osysharp.com/reference/ui/web-fonts/ -->

## Summary        {#summary}
A theme names your app's type:

```osy syntax
theme Doc { Fonts { Body = "Hedvig Letters Serif, ui-serif, Georgia, serif"; } }
```

That is a **request**, not a delivery. `font-family` is a preference list, and a browser walks it until it finds
something it already has — so on any machine without *Hedvig Letters Serif* installed, the name is skipped silently
and your app renders in `ui-serif`. Nothing errors. The page just isn't the design.

**`osy font add` is the other half.** It registers a font FILE with your app: the file is pinned by content hash in
`osyrin.lock`, carried to the server by every compile, served back immutably, and declared as an `@font-face` in the
app's stylesheet. The theme still names the family exactly as before — the two stay separate on purpose, because one
family often needs several files (a regular, a bold, an italic) and the theme should name it once.

## Signature      {#signature}
```osy syntax
osy font add <file>  --family "<CSS family name>"  [--weight <400|700|…>]  [--style <normal|italic>]
osy font list
```

## Description    {#description}

### Registering a font    {#registering}

Put the file somewhere in your project and register it:

```console
$ osy font add model/fonts/HedvigLettersSerif-Regular.woff2 --family "Hedvig Letters Serif"
✓ Registered Hedvig Letters Serif (0ab846d39150…) → model/fonts/HedvigLettersSerif-Regular.woff2
  Name it in your theme to use it, e.g.
    Fonts { Body = "Hedvig Letters Serif, ui-serif, Georgia, serif"; }
  It ships on the next compile. You are responsible for its licence.
```

`--family` is **required and never guessed**. A filename is not a family name — `HedvigLettersSerif-Regular.woff2`
provides the family `Hedvig Letters Serif` — and a guess that is subtly wrong produces the exact failure this feature
exists to remove: the font loads, nothing matches it, and the page renders in the fallback while looking healthy.

Accepted formats are `.woff2`, `.woff`, `.ttf` and `.otf`. Prefer **woff2**: it is the smallest by a wide margin and
every current browser reads it.

`osy font list` shows what your app ships:

```console
$ osy font list
╭──────────────────────┬────────┬────────┬─────────────────────────────────────╮
│ Family               │ Weight │ Style  │ File                                │
├──────────────────────┼────────┼────────┼─────────────────────────────────────┤
│ Hedvig Letters Serif │ 400    │ normal │ model/fonts/HedvigLettersSerif-Reg… │
╰──────────────────────┴────────┴────────┴─────────────────────────────────────╯
```

### Naming it in your theme    {#naming}

Registering a file does not decide where it is used — your theme does, unchanged:

```osy title="a theme that names a shipped family" test app=ui-web-fonts
theme Doc {
  Fonts {
    Body    = "Hedvig Letters Serif, ui-serif, Georgia, serif";
    Heading = "Hedvig Letters Sans, ui-sans-serif, system-ui, sans-serif";
  }
}
```

Keep the fallbacks. They are what the reader sees during the moment before the file arrives, and on the rare browser
that cannot use it at all.

### More than one file per family    {#weights}

A family is a set of faces, and each file provides one. Register each with the weight and style it covers:

```console
$ osy font add fonts/Inter-Regular.woff2    --family "Inter"
$ osy font add fonts/Inter-Bold.woff2       --family "Inter" --weight 700
$ osy font add fonts/Inter-Italic.woff2     --family "Inter" --style italic
```

`--weight` defaults to `400` and `--style` to `normal`, so the first line above needs neither. Registrations are
keyed by **(family, weight, style)**, which is why the second and third lines ADD faces rather than replace the
first. A variable font that covers a range declares it as one: `--weight "100 900"`.

Your theme still names `Inter` once. The browser picks the right file per weight and style.

### What ships, and when    {#shipping}

Every compile carries every registered font whose file has changed or that the server does not yet have, and
re-verifies each file against its pinned hash first. **A font edited on disk but never re-added aborts the compile**
rather than shipping bytes the lock does not describe:

```console
$ osy compile
✗ font 'Inter': 'fonts/Inter-Regular.woff2' has changed since it was registered (osyrin.lock pins
  9f86d0818200…, file is 2c624232945…) — re-run `osy font add fonts/Inter-Regular.woff2 --family "Inter"` to re-pin it.
```

Files are stored by content address, so identical bytes are stored once no matter how many apps or versions
reference them, and a recompile that changes nothing ships nothing.

The app's stylesheet then declares each face ahead of the rules that use it:

```css
@font-face {
  font-family: "Hedvig Letters Serif";
  src: url("/_osy/font/0ab846d39150…") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
```

`font-display: swap` is deliberate: text is painted immediately in the fallback and re-painted when the file lands.
The alternative is a first paint with nothing where the words should be.

### Removing a font from the app   {#removing}

Delete its entry from `osyrin.lock` and recompile. The next compile reconciles the server to what your app now
declares — the row, the stored bytes and the `@font-face` all go together, so a stylesheet can never keep naming a
face you stopped shipping.

### Am I allowed to self-host this font? — licensing   {#licensing}

`osy font add` ships a file you chose, and the platform makes no licence check. Web-font licences vary in ways
tooling cannot infer — some permit self-hosting freely, some by domain, some not at all. Confirm you may
self-host before you register a font.

## Examples       {#examples}

The whole loop, from a downloaded file to a page that renders in it:

```console
$ osy font add model/fonts/HedvigLettersSerif-Regular.woff2 --family "Hedvig Letters Serif"
✓ Registered Hedvig Letters Serif (0ab846d39150…) → model/fonts/HedvigLettersSerif-Regular.woff2

$ osy compile
✓ Osyrin compiled and applied to MarkdownDemo (6 file(s)).
```

with the theme naming it:

```osy title="body text in a shipped serif" test app=ui-web-fonts-example
theme Doc {
  Fonts { Body = "Hedvig Letters Serif, ui-serif, Georgia, serif"; }
}
```

To confirm a font is really being used rather than silently falling back, **measure rendered text width** against a
family you know does not exist. `document.fonts.check()` is not a test — it resolves through the fallback stack and
answers `true` either way.

## See also       {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the `theme` block, and the `Fonts` tokens that name a family
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the other kind of file an app ships with its compile, on the same rail
- [app.osy](https://osysharp.com/reference/project/manifest/) — `osyrin.lock`, where a registration is pinned
