Fuentes
Lección 18 de 37 · 6 min
Una fuente propia tiene que viajar por la red antes de dibujarse. Esta lección cubre cómo se declara, qué ve el lector mientras llega y cómo sacarle todo a un solo archivo variable.
Declarar una fuente
@font-face le pone nombre a un archivo. Después ese nombre se usa en font-family como cualquier otro. El navegador no descarga nada hasta que algún texto de la página lo necesita.
@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-style: normal;font-display: swap;}body {font-family: "Geist", Arial, sans-serif;}
- Archivo
srcAcepta una lista: el navegador usa el primer archivo cuyoformat()entiende. Hoy alcanza conwoff2.local("Arial")usa una fuente instalada en lugar de descargar. - Rango de pesos
font-weight: 100 900Es un archivo variable que cubre todos esos pesos. Con archivos estáticos se repite el bloque una vez por peso. - Caracteres
unicode-rangeDice qué caracteres trae el archivo. Si la página no usa ninguno, no se descarga.
Así carga Geist este mismo sitio: un bloque por alfabeto, todos con el mismo nombre. Un texto en español solo baja el archivo latino; el cirílico y el vietnamita quedan sin descargar.
@font-face {font-family: "Geist Variable";font-display: swap;font-weight: 100 900;src: url(./files/geist-latin-wght-normal.woff2) format("woff2-variations");unicode-range: U+0000-00FF, U+0131, U+0152-0153, /* … */ U+FFFD;}
Mientras la fuente llega
Mientras el archivo viaja, el texto que lo usa pasa por dos etapas. En el período de bloqueo el texto se dibuja invisible. En el período de intercambio se ve con la fuente de respaldo, y si la buena llega a tiempo, la reemplaza. Si llega después, se queda la de respaldo.
Ver texto invisible se llama FOIT (flash of invisible text); ver el respaldo y después el cambio, FOUT (flash of unstyled text). font-display elige el largo de cada etapa:
- Esperar
blockBloqueo corto (unos 3s) e intercambio sin fin. Para íconos hechos con fuentes, donde el respaldo mostraría letras sueltas. - Mostrar ya
swapBloqueo casi nulo e intercambio sin fin. El texto se lee enseguida; el costo es el salto. - Con plazo
fallbackBloqueo casi nulo e intercambio corto. Si tarda mucho, ni se molesta en cambiarla. - Solo si ya está
optionalBloqueo casi nulo y sin intercambio. Si no está casi enseguida, se usa en la próxima visita, ya en caché. - Que decida el navegador
autoEn la práctica suele comportarse comoblock.
Elegí un valor y mové la llegada de la fuente para ver qué tiene en pantalla el lector en cada momento.
@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-display: swap;}body {font-family: "Geist", Georgia, serif;}
/* @font-face va en el CSS de siempre */@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-display: swap;}@theme {--font-sans: "Geist", Georgia, serif;}
font-display es Baseline desde enero de 2020. Para texto de lectura, swap con un respaldo bien ajustado (más abajo) suele ser lo mejor: nadie se queda mirando una página vacía y el salto casi no se nota.
Fuentes variables
Una fuente variable trae ejes en lugar de archivos sueltos: un mismo archivo cubre cualquier peso entre 100 y 900, no solo 400 y 700. Los ejes registrados tienen etiqueta en minúsculas y una propiedad de alto nivel que los mueve:
- Peso
wght → font-weightCualquier número del rango, como560. - Ancho
wdth → font-stretchEn porcentaje, como87.5%para una versión más angosta. - Tamaño óptico
opsz → font-optical-sizingConautoajusta el dibujo al tamaño: trazos más finos y apretados en títulos, más abiertos en texto chico. - Cursiva e inclinación
ital, slnt → font-stylefont-style: italicuoblique 10deg.
font-variation-settings mueve cualquier eje a mano, incluidos los propios de cada fuente, que van en mayúsculas (como "GRAD"). Para los registrados conviene la propiedad de alto nivel: font-variation-settings no se hereda por eje y no le avisa al resto del CSS. Con "wght" 600, font-weight sigue valiendo 400, así que bolder y la elección de archivos no se enteran.
Geist, la fuente de este sitio, solo tiene el eje wght. Mové el peso y cambiá la forma de escribirlo: el dibujo es el mismo, pero el valor que calcula el CSS no.
Jovencillo emponzoñado de whisky: ¡qué figurota exhibe!
/* así carga Geist este sitio (subconjunto latino) */@font-face {font-family: "Geist Variable";font-style: normal;font-display: swap;font-weight: 100 900;src: url(./files/geist-latin-wght-normal.woff2) format("woff2-variations");unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}.muestra {font-family: "Geist Variable", sans-serif;font-weight: 560;}
<p class="font-sans font-[560]">…</p>
/* una fuente con más ejes, como Roboto Flex */.titulo {font-weight: 650;font-stretch: 87.5%;font-optical-sizing: auto;font-variation-settings: "GRAD" 50; /* eje propio: solo así */}
font-variation-settings, font-stretch en porcentaje y font-optical-sizing son Baseline desde hace años: entre 2018 y 2020 según la propiedad.
Funciones OpenType
Muchas fuentes traen glifos alternativos que se activan con una etiqueta de cuatro letras: tnum para cifras del mismo ancho, smcp para versalitas, frac para fracciones. Hay dos formas de pedirlas:
- De alto nivel
font-variant-*font-variant-numeric: tabular-nums,font-variant-caps: small-caps. Se suman entre sí y, si la fuente no tiene versalitas, el navegador las sintetiza achicando las mayúsculas. - De bajo nivel
font-feature-settings: "tnum"Llega a cualquier etiqueta, pero cada regla reemplaza la lista entera y, si la fuente no trae la función, no pasa nada.
En un horario, las cifras proporcionales de Geist hacen que 11:11 sea más angosto que 10:58 y las columnas no se alinean. Con tabular-nums todas las horas miden lo mismo. Ojo con las versalitas: Geist no trae smcp, así que solo font-variant-caps hace algo.
Un respaldo que no salte
Con swap, el texto se dibuja primero con la fuente de respaldo. Si sus letras son más anchas o más altas, cuando llega la buena las líneas cortan en otro lugar y todo lo de abajo se mueve. Dentro de @font-face hay descriptores para deformar el respaldo hasta que mida casi lo mismo:
- Escala
size-adjustEscala los glifos (y el ancho) en porcentaje. Chrome y Firefox lo soportan desde la 92, Safari desde la 17. - Altura de línea
ascent-, descent-, line-gap-overrideFijan la altura sobre y bajo la línea de base, que define el alto de cada línea conline-height: normal. Chrome desde la 87 y Firefox desde la 89; Safari todavía no, así que no son Baseline.
El párrafo naranja es Arial, lo que se vería antes de que llegue Geist. A la izquierda, sin tocar; a la derecha, con size-adjust y los overrides: casi no se nota cuál es cuál.
Los valores exactos salen de las métricas de las dos fuentes. Herramientas como Capsize (o next/font, que lo hace solo) los calculan por vos; este sitio usa size-adjust: 104.7553% sobre Arial.
Recortar el interlineado
Cada línea de texto reserva lugar arriba y abajo de las letras: el que define la fuente más la mitad del interlineado de cada lado. Por eso un título nunca queda pegado al borde de su caja, y los márgenes que medís en el diseño no coinciden con los del navegador.
text-box-trim recorta ese espacio en la primera línea (trim-start), en la última (trim-end) o en las dos (trim-both). text-box-edge dice hasta dónde: cap corta en la altura de las mayúsculas, ex en la de las minúsculas, alphabetic en la línea de base y text en los bordes de la fuente. El atajo text-box: trim-both cap alphabetic junta las dos.
Chrome lo soporta desde la 133 y Safari desde la 18.2; los datos de MDN lo dan en Firefox desde la 154, pero todavía no figura como Baseline. Sin soporte, la caja queda con su espacio normal, así que es seguro usarlo: solo cambia el ajuste fino.
En Tailwind
@font-face va en el CSS de siempre. Para usar la familia como clase, registrala en @theme: --font-sans reemplaza la de font-sans, y un nombre nuevo como --font-titulo crea la clase font-titulo.
| CSS | Tailwind |
|---|---|
| font-weight: 560 | font-[560] |
| font-weight: 500 · 700 | font-medium · font-bold |
| font-stretch: 87.5% | font-stretch-[87.5%] |
| font-variation-settings: "GRAD" 50 | [font-variation-settings:'GRAD'_50] |
| font-variant-numeric: tabular-nums | tabular-nums |
| font-variant-caps: small-caps | [font-variant-caps:small-caps] |
| font-feature-settings: "tnum" | [font-feature-settings:'tnum'] |
| text-box: trim-both cap alphabetic | [text-box:trim-both_cap_alphabetic] |
@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-display: swap;}@theme {--font-sans: "Geist", "Geist respaldo", sans-serif;}
Fonts
Lesson 18 of 37 · 6 min
A custom font has to travel over the network before it can be drawn. This lesson covers how to declare one, what the reader sees while it loads, and how to get everything out of a single variable file.
Declaring a font
@font-face gives a file a name, which you then use in font-family like any other. The browser downloads nothing until some text on the page needs it.
@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-style: normal;font-display: swap;}body {font-family: "Geist", Arial, sans-serif;}
- File
srcTakes a list: the browser uses the first file whoseformat()it understands. Todaywoff2is all you need.local("Arial")uses an installed font instead of downloading. - Weight range
font-weight: 100 900One variable file covering all those weights. With static files you repeat the block once per weight. - Characters
unicode-rangeLists the characters the file covers. If the page uses none of them, it isn’t downloaded.
This is how this very site loads Geist: one block per script, all under the same name. English text only fetches the latin file; Cyrillic and Vietnamese are never downloaded.
@font-face {font-family: "Geist Variable";font-display: swap;font-weight: 100 900;src: url(./files/geist-latin-wght-normal.woff2) format("woff2-variations");unicode-range: U+0000-00FF, U+0131, U+0152-0153, /* … */ U+FFFD;}
While the font loads
While the file travels, text that uses it goes through two stages. In the block period it’s drawn invisible. In the swap period it shows in the fallback font, and if the real one arrives in time it takes over. If it arrives later, the fallback stays.
Seeing invisible text is a FOIT (flash of invisible text); seeing the fallback and then the change is a FOUT (flash of unstyled text). font-display picks how long each stage lasts:
- Wait
blockA short block period (about 3s) and an endless swap. For icon fonts, where a fallback would show stray letters. - Show it now
swapAlmost no block and an endless swap. Text is readable right away; the price is the jump. - With a deadline
fallbackAlmost no block and a short swap. If the font is slow, it doesn’t bother switching. - Only if it’s there
optionalAlmost no block and no swap. If the font isn’t there almost at once, it’s used on the next visit, from the cache. - Let the browser decide
autoIn practice it usually behaves likeblock.
Pick a value and move the font’s arrival to see what the reader has on screen at each moment.
@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-display: swap;}body {font-family: "Geist", Georgia, serif;}
/* @font-face goes in plain CSS */@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-display: swap;}@theme {--font-sans: "Geist", Georgia, serif;}
font-display has been Baseline since January 2020. For body text, swap with a well-matched fallback (below) is usually best: nobody stares at an empty page and the jump is barely visible.
Variable fonts
A variable font ships axes instead of separate files: one file covers any weight from 100 to 900, not just 400 and 700. Registered axes have lowercase tags and a high-level property that drives them:
- Weight
wght → font-weightAny number in the range, such as560. - Width
wdth → font-stretchAs a percentage, such as87.5%for a narrower cut. - Optical size
opsz → font-optical-sizingWithautoit adapts the design to the size: finer, tighter strokes in headings, more open in small text. - Italic and slant
ital, slnt → font-stylefont-style: italicoroblique 10deg.
font-variation-settings sets any axis by hand, including a font’s own custom axes, which are uppercase (like "GRAD"). For registered axes prefer the high-level property: font-variation-settings doesn’t cascade per axis and tells the rest of CSS nothing. With "wght" 600, font-weight is still 400, so bolder and font matching never find out.
Geist, this site’s font, only has the wght axis. Move the weight and switch how it’s written: the drawing is the same, the value CSS computes isn’t.
Sphinx of black quartz, judge my vow.
/* how this site loads Geist (latin subset) */@font-face {font-family: "Geist Variable";font-style: normal;font-display: swap;font-weight: 100 900;src: url(./files/geist-latin-wght-normal.woff2) format("woff2-variations");unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}.specimen {font-family: "Geist Variable", sans-serif;font-weight: 560;}
<p class="font-sans font-[560]">…</p>
/* a font with more axes, such as Roboto Flex */.heading {font-weight: 650;font-stretch: 87.5%;font-optical-sizing: auto;font-variation-settings: "GRAD" 50; /* custom axis: only this way */}
font-variation-settings, percentage font-stretch and font-optical-sizing have all been Baseline for years, since 2018 to 2020 depending on the property.
OpenType features
Many fonts carry alternate glyphs switched on by a four-letter tag: tnum for equal-width figures, smcp for small caps, frac for fractions. There are two ways to ask for them:
- High level
font-variant-*font-variant-numeric: tabular-nums,font-variant-caps: small-caps. They add up, and if the font has no small caps the browser synthesises them by shrinking the capitals. - Low level
font-feature-settings: "tnum"It reaches any tag, but every rule replaces the whole list, and if the font lacks the feature nothing happens.
In a timetable, Geist’s proportional figures make 11:11 narrower than 10:58 and the columns drift. With tabular-nums every time is as wide as the others. Mind the small caps: Geist has no smcp, so only font-variant-caps does anything.
A fallback that doesn’t jump
With swap, text is drawn in the fallback first. If its letters are wider or taller, lines break elsewhere once the real font lands and everything below moves. @font-face has descriptors to reshape the fallback until it measures almost the same:
- Scale
size-adjustScales the glyphs (and their widths) by a percentage. Chrome and Firefox support it since 92, Safari since 17. - Line height
ascent-, descent-, line-gap-overrideSet the height above and below the baseline, which sizes each line underline-height: normal. Chrome since 87 and Firefox since 89; Safari doesn’t yet, so they aren’t Baseline.
The orange paragraph is Arial, what you’d see before Geist arrives. On the left, untouched; on the right, with size-adjust and the overrides: you can hardly tell which is which.
The exact values come from both fonts’ metrics. Tools like Capsize (or next/font, which does it for you) work them out; this site uses size-adjust: 104.7553% on Arial.
Trimming the leading
Every line of text keeps room above and below its letters: the font’s own, plus half the leading on each side. That’s why a heading never sits flush with its box, and why the spacing you measure in a design tool doesn’t match the browser’s.
text-box-trim removes that room on the first line (trim-start), the last (trim-end) or both (trim-both). text-box-edge says how far: cap trims to the capital height, ex to the x-height, alphabetic to the baseline and text to the font’s edges. The shorthand text-box: trim-both cap alphabetic sets both.
Chrome supports it since 133 and Safari since 18.2; MDN’s data lists Firefox 154, but it isn’t marked Baseline yet. Without support the box keeps its usual room, so it’s safe to use: only the fine-tuning changes.
In Tailwind
@font-face goes in plain CSS. To use the family as a class, register it in @theme: --font-sans replaces the one behind font-sans, and a new name such as --font-heading creates a font-heading class.
| CSS | Tailwind |
|---|---|
| font-weight: 560 | font-[560] |
| font-weight: 500 · 700 | font-medium · font-bold |
| font-stretch: 87.5% | font-stretch-[87.5%] |
| font-variation-settings: "GRAD" 50 | [font-variation-settings:'GRAD'_50] |
| font-variant-numeric: tabular-nums | tabular-nums |
| font-variant-caps: small-caps | [font-variant-caps:small-caps] |
| font-feature-settings: "tnum" | [font-feature-settings:'tnum'] |
| text-box: trim-both cap alphabetic | [text-box:trim-both_cap_alphabetic] |
@font-face {font-family: "Geist";src: url(/fonts/geist.woff2) format("woff2");font-weight: 100 900;font-display: swap;}@theme {--font-sans: "Geist", "Geist fallback", sans-serif;}