# Webfonts

woff2, font-display, variable fonts en subsetting, of hoe je een font laadt zonder je pagina te vertragen

Multimedia & Creatieve Technologie · 2.1 Web production · Bite · versie 1.0.0 · 2026-10-17

Tags: css, performance, fonts

Slides: <https://slides.mctantwerp.be/nl/web-production/webfonts>

<!-- slide 2 -->

## Na deze les kan je

- Een font zelf hosten met @font-face en woff2
- Kiezen wat de browser toont zolang je font laadt
- Een variable font gebruiken in plaats van vijf bestanden
- Een font kleiner maken met subsetting
- Een fallback instellen die je layout niet laat springen
- Uitleggen wanneer je beter geen webfont gebruikt

> **Toelichting:** Het laatste punt is het belangrijkste: een font is een keuze met een prijs. Soms is het systeemfont van de bezoeker de betere keuze, en dat is geen luiheid.

<!-- slide 3 -->

## Wat een font kost

Een extra download

De browser ontdekt je font pas als hij de CSS gelezen heeft en weet dat een element het nodig heeft.

Onzichtbare tekst

Wacht de browser op het font, dan zie je geen tekst. Dat heet FOIT, flash of invisible text.

Tekst die springt

Toont hij eerst een fallback, dan wisselt het font later en verschuift je layout. Dat heet FOUT.

Meer op [MDN: Web fonts](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Text_styling/Web_fonts)

> **Toelichting:** Open DevTools, tab Network, filter op Font, en herlaad een site die je vaak gebruikt. Je ziet hoeveel fonts er laden en hoe zwaar ze zijn. Zet de throttling op Slow 4G en kijk wat er gebeurt met de tekst: verdwijnt ze even, of springt ze? Dat is FOIT of FOUT, en allebei kan je sturen.

<!-- slide 4 -->

## Enkel woff2

```css
@font-face {
  font-family: 'Anton';
  src: url('../fonts/anton.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

h1 {
  font-family: 'Anton', Impact, sans-serif;
}
```

Je geeft het font een naam in `font-family` en wijst naar het bestand. Daarna gebruik je die naam zoals elk ander font, met fallbacks erachter.

woff2 is gecomprimeerd en werkt in elke browser. Een tweede formaat ernaast heb je niet meer nodig.

[MDN: @font-face](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face)

> **Toelichting:** Vroeger zag je @font-face met vijf formaten: eot voor Internet Explorer, ttf, svg, woff en woff2. Dat is voorbij, elke browser die je moet ondersteunen kan woff2. Heb je enkel een ttf of otf, zet hem dan om, bijvoorbeeld met pyftsubset van verderop in deze bite. Let op font-weight en font-style: die zeggen bij welk gewicht dit bestand hoort. Zonder die regels gaat de browser een vet font zelf namaken, en dat ziet er slecht uit.

<!-- slide 5 -->

## font-display

| Waarde | Zolang het font laadt | Goed voor |
| --- | --- | --- |
| `block` | Tot ongeveer 3 seconden geen tekst, dan een fallback | Een icoonfont, waar een fallback onzin toont |
| `swap` | Meteen een fallback, wisselen zodra het font er is | Titels en tekst die je meteen wil lezen |
| `fallback` | Heel kort niets, dan een fallback. Na ongeveer 3 seconden wisselt het niet meer | Lopende tekst |
| `optional` | Heel kort niets. Is het font er dan niet, dan blijft de fallback | Een site waar niets mag springen |

[MDN: font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face/font-display)

> **Toelichting:** Elk font krijgt een block period, waarin de tekst onzichtbaar blijft, en een swap period, waarin de browser nog mag wisselen naar het webfont. swap is de veilige keuze voor tekst die je meteen moet kunnen lezen. optional is de beste voor je layout: is het font er niet binnen ongeveer 100ms, dan blijft de fallback staan, en bij een volgend bezoek komt het font uit de cache. Gebruik je Google Fonts, dan zet &display=swap in de url precies deze regel.

<!-- slide 6 -->

## Preload

```html
<link
  rel="preload"
  href="/fonts/anton.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>
```

Zonder preload vindt de browser je font pas na de CSS. Met preload start de download meteen, in de head van je HTML.

Enkel voor het font dat je bovenaan de pagina ziet. Alles preloaden is hetzelfde als niets preloaden.

[MDN: rel=preload](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload)

> **Toelichting:** crossorigin is verplicht bij fonts, ook als het font van je eigen domein komt. Zonder dat attribuut downloadt de browser het font twee keer: één keer voor de preload en nog eens als de CSS erom vraagt. Je ziet het in de Network-tab, en Chrome waarschuwt in de console dat een preload niet gebruikt werd.

<!-- slide 7 -->

## Eén bestand, elk gewicht

Een slider over de as wght van Inter Variable: de tekst "Webfonts zonder wachten" gaat van gewicht 100 tot 900, uit één bestand, met de CSS die bij de positie hoort.

Een variable font heeft assen, zoals het gewicht `wght`. Elke waarde op die as kan, uit één bestand. [MDN: Variable fonts](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Fonts/Variable_fonts)

> **Toelichting:** Dit is Inter Variable, het font van deze slides. Eén bestand bevat elk gewicht van 100 tot 900. Met vaste fonts heb je een bestand per gewicht, en al vanaf drie of vier gewichten is een variable font meestal lichter. Gebruik je maar één gewicht, dan is een vast font kleiner. Op Google Fonts zie je bij een font of het variable is: er staat een slider bij.

<!-- slide 8 -->

## font-weight of font-variation-settings

```css
@font-face {
  font-family: 'Inter';
  src: url('inter-variable.woff2') format('woff2');
  font-weight: 100 900;
}

h1 { font-weight: 750; }
.wide { font-stretch: 115%; }

/* een eigen as van het font */
.soft { font-variation-settings: 'SOFT' 100; }
```

In `@font-face` geef je het bereik op: `font-weight: 100 900`. Daarna kan elk getal ertussen.

Voor gewicht en breedte neem je `font-weight` en `font-stretch`, niet `font-variation-settings`.

> **Toelichting:** font-variation-settings is de ruwe toegang tot de assen. Het probleem: het overschrijft alle assen tegelijk, en een font-weight verderop in je CSS wint er niet van. Gebruik dus altijd de gewone properties voor de vijf geregistreerde assen (gewicht, breedte, schuin, cursief, optische grootte), en font-variation-settings enkel voor een eigen as van het font, die je herkent aan de hoofdletters.

<!-- slide 9 -->

## Enkel de tekens die je nodig hebt

Een font bevat vaak honderden tekens die je pagina nooit toont. Haal ze eruit met pyftsubset:

```console
$ pyftsubset Anton-Regular.ttf --unicodes="U+0020-007E" --flavor=woff2 --output-file=anton.woff2
```

[fontTools: pyftsubset](https://fonttools.readthedocs.io/en/latest/subset/)

Bij Google Fonts zet je de tekens in de url met `text=`. Ideaal voor een titel of een banner met vaste tekst.

```html
<link href="https://fonts.googleapis.com/css2?family=Anton&text=JUSTDOIT" rel="stylesheet">
```

[Google Fonts: text=](https://developers.google.com/fonts/docs/getting_started#optimizing_your_font_requests)

> **Toelichting:** pyftsubset hoort bij fontTools, een Python-pakket: pip install fonttools brotli. Brotli heb je nodig voor woff2. Kijk wel naar de licentie van je font: de meeste open source fonts (de OFL van Google Fonts) laten subsetting toe, een gekocht font soms niet. Een tweede voordeel van subsetten: unicode-range in @font-face laat de browser een bestand enkel downloaden als de pagina een teken uit dat bereik gebruikt. Zo doet Google Fonts het zelf, met een apart bestand voor Latijn, Cyrillisch en Grieks.

<!-- slide 10 -->

## Google Fonts of zelf hosten

### Via Google

Snel ingesteld met één link. Maar elke bezoeker stuurt zijn IP-adres naar Google.

Een Duitse rechtbank vond dat in 2022 in strijd met de GDPR. [LG München I](https://www.ra-plutte.de/lg-muenchen-dynamische-einbindung-google-web-fonts-ist-dsgvo/)

### Zelf hosten

Het woff2-bestand staat op je eigen server, naast je CSS. Geen extra domein, geen data naar Google.

[Fontsource](https://fontsource.org/) zet een font in je project met npm. [google-webfonts-helper](https://gwfh.mranftl.com/fonts) geeft je de bestanden en de CSS.

> **Toelichting:** Het Landgericht München I besliste op 20 januari 2022 dat een site die Google Fonts rechtstreeks laadt, het IP-adres van een bezoeker zonder toestemming aan Google doorgeeft. De eigenaar moest 100 euro schadevergoeding betalen. Het is een Duitse uitspraak van een lagere rechtbank, maar ze gaat over de GDPR, en die geldt ook hier. Daarna kregen veel Duitse sites een brief van een advocaat met een eis tot betaling. Zelf hosten lost het op en is meestal ook sneller: geen extra verbinding met een ander domein.

<!-- slide 11 -->

## Een fallback die past

```css
@font-face {
  font-family: 'Inter fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
}

body {
  font-family: 'Inter', 'Inter fallback', sans-serif;
}
```

Je maakt een tweede font van Arial, geschaald tot het even breed en hoog is als je webfont.

Wisselt de browser naar het webfont, dan blijft je tekst op dezelfde plek en springt je layout niet.

[MDN: size-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face/size-adjust)

> **Toelichting:** De getallen haal je niet uit je duim. Tools zoals Fontaine of de fallback-generator van Next.js rekenen ze uit op basis van de metrics van beide fonts. ascent-override en descent-override werken in Chrome en Firefox, Safari kent enkel size-adjust. Zie het als een verbetering: in Safari springt het nog een beetje, nergens is het slechter dan zonder.

<!-- slide 12 -->

## Of geen webfont

```css
body {
  font-family: system-ui, -apple-system,
    'Segoe UI', Roboto, sans-serif;
}
```

Het font dat al op het toestel staat, laadt in nul milliseconden en springt nooit.

San Francisco op een Mac, Segoe UI op Windows, Roboto op Android.

[Modern Font Stacks](https://modernfontstacks.com/)

> **Toelichting:** GitHub, Bootstrap en de admin van WordPress gebruiken gewoon het systeemfont. Voor een dashboard of een webapp die mensen elke dag openen, is dat een goede keuze: het laadt nooit, het leest vertrouwd en het past bij het toestel. Voor een merk dat herkend moet worden, zoals Nike in de banners, is het font net deel van de identiteit. Op Modern Font Stacks vind je stacks in elke stijl.

<!-- slide 13 -->

## Je Nike-banner weegt 185 kB gezipt, Anton zit erin als volledige ttf. Wat doe je eerst?

- A. Anton van Google Fonts laden in plaats van uit de zip
- B. GSAP weglaten en alles in CSS animeren
- C. Anton omzetten naar woff2 met enkel de tekens van je banner
- D. De afbeeldingen als png exporteren

**Antwoord:** C. Anton omzetten naar woff2 met enkel de tekens van je banner

Een banner met vaste tekst gebruikt hooguit een paar tientallen tekens. Een subset in woff2 weegt dan een fractie van de volledige ttf. Google Fonts lijkt handig, maar een banner moet alles in de zip hebben, en de bezoeker stuurt zijn IP-adres naar Google. png maakt foto's net zwaarder.

> **Toelichting:** De 150 kB van een banner telt alles: HTML, CSS, JavaScript, beelden en fonts. Begin altijd bij het grootste bestand in je map, en dat is vaak het font. Een ttf bevat ook alle hinting en alle tekens, woff2 comprimeert bovendien met Brotli.

<!-- slide 14 -->

## Thuis: Je eigen font, zelf gehost

1. Kies een font op Google Fonts en download het, of haal het met Fontsource
2. Zet het als woff2 in je project en schrijf zelf de @font-face
3. Kies een font-display en schrijf in een commentaar waarom
4. Preload het font dat je bovenaan de pagina ziet
5. Klaar als de Network-tab enkel fonts van je eigen domein toont, en elk woff2-bestand onder 50 kB blijft

> **Toelichting:** Begin klein: één font, één gewicht. Wie een variable font kiest, zet in @font-face het bereik (font-weight: 100 900), anders kan je enkel het standaardgewicht gebruiken. Zie je in DevTools nog een request naar fonts.googleapis.com of fonts.gstatic.com, dan laadt er ergens nog een link of een @import.

## Begrippen

- **Webfont**: Een font dat de browser van je server of een andere server downloadt, in plaats van een font dat al op de computer van de bezoeker staat. (slide 3)
- **FOIT**: Flash of invisible text: de browser toont de tekst niet zolang het webfont laadt. Op een trage verbinding zie je seconden lang een lege pagina. (slide 3)
- **FOUT**: Flash of unstyled text: de browser toont de tekst eerst in een fallback font en wisselt als het webfont binnen is. Je kan meteen lezen, maar de tekst springt. (slide 3)
- **woff2**: Het fontformaat voor het web, met Brotli gecomprimeerd. Elke browser van vandaag kan het, dus een ander formaat ernaast heb je niet meer nodig. (slide 4)
- **font-display**: Descriptor in @font-face die zegt wat de browser doet zolang een font laadt: wachten (block), meteen een fallback tonen en later wisselen (swap), of het font enkel gebruiken als het snel genoeg is (optional). (slide 5)
- **Variable font**: Eén fontbestand met assen, zoals het gewicht (wght) of de breedte (wdth). Elke waarde op die as kan, in plaats van één bestand per gewicht. (slide 7)
- **Subsetting**: Een font kleiner maken door de tekens eruit te halen die je pagina niet gebruikt, zoals Cyrillisch of Grieks. (slide 9)
- **size-adjust**: Descriptor in @font-face die een font groter of kleiner schaalt. Zo maak je een fallback font even breed als je webfont, en springt je layout niet als het webfont binnenkomt. (slide 11)

## Links en bronnen

### In de presentatie

- [MDN: Web fonts](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Text_styling/Web_fonts) (slide 3)
- [MDN: @font-face](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face) (slide 4)
- [MDN: font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face/font-display) (slide 5)
- [MDN: rel=preload](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) (slide 6)
- [MDN: Variable fonts](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Fonts/Variable_fonts) (slide 7)
- [fontTools: pyftsubset](https://fonttools.readthedocs.io/en/latest/subset/) (slide 9)
- [Google Fonts: CSS API, text=](https://developers.google.com/fonts/docs/getting_started#optimizing_your_font_requests) (slide 9): Een font met enkel de tekens die je meegeeft
- [LG München I, 3 O 17493/20](https://www.ra-plutte.de/lg-muenchen-dynamische-einbindung-google-web-fonts-ist-dsgvo/) (slide 10): 20 januari 2022, 100 euro schadevergoeding voor een IP-adres dat naar Google ging
- [Fontsource](https://fontsource.org/) (slide 10): Open source fonts als npm package, om zelf te hosten
- [google-webfonts-helper](https://gwfh.mranftl.com/fonts) (slide 10)
- [MDN: size-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face/size-adjust) (slide 11)
- [Modern Font Stacks](https://modernfontstacks.com/) (slide 12)

### Bronnen

- [MDN: unicode-range](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face/unicode-range)
- [Can I use: ascent-override](https://caniuse.com/mdn-css_at-rules_font-face_ascent-override)
