# 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 { Font { Sans = "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 add --from-kit <Kit>  --family "<CSS family name>"
osy font add --fontsource <id>  [--weight 400,700]  [--style italic]
osy font find "<character>"
osy font kits
osy font list
osy font remove "<CSS family name>"
```

## 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. Shadow the kit's own token, so every
  control picks it up without touching a page:
    Font { Sans = "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 {
  Font {
    Sans    = "Hedvig Letters Serif, ui-serif, Georgia, serif";        // every control's text — the kit's own token
    Display = "Hedvig Letters Sans, ui-sans-serif, system-ui, sans-serif";  // a second face, applied by name
  }
}
```

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.

### Where it applies, without touching a page    {#where-it-applies}

The page's own body takes its family from the theme, so naming one is all it takes — every control inherits it, and
there is no root element to go and edit. Two token spellings are read, in this order, and the platform's system stack
is the last resort:

```css
font-family: var(--font-sans, var(--fonts-body, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif));
```

**Name it `Font { Sans = … }`** — the kit's own token, which every control reads, so shadowing it re-skins them all.
The page body also reads `--fonts-body` as a fallback, but no CONTROL does, and `Fonts { Body = … }` collides with the
`FontSize { Body }` most themes declare: a theme's leaf names are one flat namespace across its groups, so the compiler
refuses `Body` twice ("duplicate token 'Body'"). `Font { Sans }` has neither problem. Monospaced chrome reads `--font-mono`, then
`--fonts-mono`.

A family you name in neither place still applies wherever you spell it out: `fontFamily: Font.Display` on a `Text`,
or `FontFamily = Font.Display;` in a variant. That is how a second face used only for headings is applied.

⚠ The **last** entry in the stack is the degrade path, and it is the reason to keep one. A face that fails to load —
a lock that pins bytes the app no longer ships, a network that never answers — falls to the next name you wrote. With
nothing after the family, it falls to whatever the browser defaults to, which is a different design on every machine.

### 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 registers the range it covers by itself** — `osy font add` reads the file's `wght` axis, so
a 100–900 face is registered as `100 900` and every weight renders from it rather than being synthesised. `--weight`
still overrides what the file says.

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.

### The two faces every app already has    {#built-in}
The platform ships **Mona Sans** (GitHub's, variable weight 200–900) and **Martian Mono** (Evil Martians, variable
weight 100–800), both under the SIL Open Font License, and declares them in every app's stylesheet: the
`Osysharp.Ui` theme sets `Font.Sans` and `Font.Mono` in them, so an app on the kit's own look renders in them with no
`osy font add`. A declared face costs nothing until text is set in it — a browser fetches a font only when a page
uses it — so an app that names its own `Sans` never downloads Mona Sans. An app's own face of the same family wins.

### A font a kit already ships   {#from-a-kit}

A look kit carries its own faces, each with its licence beside it. `osy font kits` lists every font the kits in your
project's scope ship — the family, the kit, and the licence file found next to it. To use one in your own theme, take
it into the app:

```console
osy font kits
osy font add --from-kit Studio --family "Hanken Grotesk"
```

Every file of that family is copied into `model/fonts/` with its licence text, and pinned exactly as a file of your own
would be. The kit may be named in full or by its last part.

### An open-source face, by its character   {#fontsource}

`osy font find "<character>"` searches the open-source families by what they are like — `calm humanist`,
`rounded geometric`, `sophisticated didone` — and `osy font add --fontsource <id>` fetches the one you chose, vendors it
with its licence and registers it under its real family name, with no file to download first:

```console
osy font find "rounded geometric"
osy font add --fontsource comfortaa
```

[Finding a typeface by its character, and shipping it](https://osysharp.com/reference/local/finding-a-font/) has the whole of both.

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

`osy font remove "<family>"` takes every entry of the family out of `osyrin.lock` and deletes the files it vendored. 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 {
  Font { Sans = "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 `Font` 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
- [Finding a typeface by its character, and shipping it](https://osysharp.com/reference/local/finding-a-font/) — find an open-source face by its character, and fetch it
