wandres.dev
FUENTES WEB · @font-face, subsetting, font-display

@font-face y sus descriptores

Cómo empareja el navegador un elemento con una cara concreta, qué hace cada descriptor, y por qué los de métricas son la única solución real al reflujo del intercambio.

⏱ 18 min

@font-face no es una regla de estilo: es un bloque de descriptores que registra una cara en la lista de fuentes disponibles del documento. No cascadea, no se hereda y no se aplica a nadie. Entender que lo que declaras es una entrada de catálogo, y que el emparejamiento es otro algoritmo distinto, evita la mitad de los errores tipográficos que parecen inexplicables.

🎯 Al terminar esta lección sabrás
  • Explicar qué registra @font-face y cómo se empareja después.
  • Usar src con format() y tech() para dar alternativas ordenadas.
  • Declarar rangos de peso y de estilo para una fuente variable.
  • Ajustar las métricas de una fuente de respaldo con size-adjust y las sobreescrituras.

Un catálogo, no una regla

@font-face {
  font-family: 'Texto';
  src: url('/f/texto-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

Esto no aplica nada. Añade a la lista de fuentes del documento una entrada que dice: existe una cara llamada Texto, con peso 400 y estilo normal, y su archivo está aquí.

Después, cuando una regla de estilo pide font-family: 'Texto' con font-weight: 600, el algoritmo de emparejamiento recorre esa lista buscando la cara que mejor encaje. Si no hay ninguna con peso 600, coge la más próxima según reglas de proximidad y, si el navegador lo permite, sintetiza el peso que falta emborronando los trazos.

De ahí salen dos errores clásicos.

Declarar un peso que no corresponde al archivo. Si el archivo es la negrita y lo declaras como font-weight: 400, el navegador lo usará para el texto normal —que se verá pesado— y para la negrita sintetizará una negrita encima de una negrita, con el resultado empastado que se reconoce a la legua.

No declarar los pesos que sí tienes. Si tienes archivos de 400 y 700 pero solo declaras el 400, el 700 se sintetiza pese a estar el archivo real ahí al lado.

La síntesis se controla con font-synthesis y sus longhands font-synthesis-weight, font-synthesis-style y font-synthesis-small-caps. Desactivarla es una buena forma de descubrir el problema: si al poner font-synthesis: none la negrita desaparece, es que nunca hubo un archivo de negrita.

src y el orden de preferencia

src es una lista ordenada. El navegador coge la primera entrada que sabe usar y no mira las demás.

@font-face {
  font-family: 'Marca';
  src: local('Marca Sans'),
       url('/f/marca.woff2') format('woff2') tech(variations),
       url('/f/marca-400.woff2') format('woff2');
}

local() intenta usar una fuente instalada en el sistema con ese nombre. Fue popular hace años y hoy se desaconseja: los nombres locales varían entre plataformas, es fácil coger una versión antigua o distinta de la que esperas, y en muchos navegadores el ahorro no compensa el riesgo de que el usuario vea otra tipografía.

format() indica el formato del archivo, y tech() la tecnología que requiere: variations, palettes, color-COLRv1, features-opentype, incremental y varias más. Las dos funcionan como filtro previo: si el navegador no soporta ese formato o esa tecnología, descarta la entrada sin descargarla. Eso convierte la lista de src en el mecanismo de detección natural para fuentes, sin necesidad de ninguna at-rule condicional. tech() en src está en Chrome 108, Firefox 107 y Safari 17.

En 2026 la práctica razonable es una sola entrada woff2 y nada más. WOFF2 está en todos los motores desde hace una década, comprime con Brotli, y mantener un woff de reserva es peso muerto en la hoja de estilos y una entrada más que el navegador tiene que evaluar.

Los descriptores que definen el emparejamiento

Cuatro descriptores participan en la selección de la cara.

font-weight acepta un número o un rango, y el rango es lo que declara una fuente variable:

@font-face {
  font-family: 'Texto';
  src: url('/f/texto-var.woff2') format('woff2') tech(variations);
  font-weight: 100 900;
  font-style: normal;
}

Con eso, cualquier font-weight entre 100 y 900 —incluido font-weight: 437— empareja con esta cara, y el navegador instancia el eje correspondiente. Sin el rango, la fuente variable se comportaría como una estática de un solo peso.

font-style acepta normal, italic u oblique con un rango de ángulos: font-style: oblique 0deg 12deg declara una cara capaz de inclinarse hasta 12 grados.

font-stretch acepta un porcentaje o un rango, y es el que empareja con el eje de anchura.

unicode-range restringe la cara a un conjunto de puntos de código, y es a la vez un filtro de emparejamiento y un mecanismo de carga diferida.

Los descriptores de métricas

Aquí está la herramienta que de verdad resuelve el reflujo del intercambio, y la que casi nadie usa.

El problema: la fuente de respaldo y la fuente web tienen métricas distintas —altura de la x, anchos de avance, ascendente y descendente—, con lo que además cambian de valor las unidades que miden glifos, así que al cambiar de una a otra el texto ocupa un espacio diferente y todo se mueve. Los descriptores de ajuste permiten deformar la fuente de respaldo hasta que ocupe exactamente lo mismo que la web, con lo cual el intercambio deja de mover nada.

/* la fuente web real */
@font-face {
  font-family: 'Texto';
  src: url('/f/texto.woff2') format('woff2');
  font-display: swap;
}

/* una cara de respaldo ajustada a sus metricas */
@font-face {
  font-family: 'Texto respaldo';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

body { font-family: 'Texto', 'Texto respaldo', sans-serif; }

size-adjust escala los contornos de los glifos por un porcentaje, ajustando de paso los avances horizontales; es la que corrige el ancho de línea. ascent-override, descent-override y line-gap-override fijan las métricas verticales como porcentaje del tamaño de fuente, y son las que corrigen la altura de la caja de línea.

Los números salen de comparar las métricas de las dos fuentes, y hay herramientas que los calculan; también se puede hacer a mano midiendo el ancho de un párrafo largo con una y con otra y ajustando size-adjust hasta que coincidan.

⚠️
Safari no implementa las sobreescrituras de métricas

size-adjust está en los tres motores —Chrome 92, Firefox 92 y Safari 17—, pero ascent-override, descent-override y line-gap-override están en Chromium y Firefox y no en Safari. Y como son descriptores y no propiedades, @supports no puede detectarlos. La consecuencia práctica: la corrección horizontal con size-adjust funciona en todas partes, la vertical no. Diseña para que el resultado sin las sobreescrituras siga siendo aceptable, no para que sea distinto.

El respaldo no es un plan B, es la mitad del diseño tipográfico

Casi todo el mundo escribe font-family: 'Marca', sans-serif y considera terminada la decisión. Pero esa segunda entrada es lo que ve el usuario durante los primeros cientos de milisegundos de cada primera visita, que estadísticamente es una fracción enorme de todas las impresiones de tu tipografía; en conexiones malas es lo único que ven algunos usuarios; y con font-display: optional es lo que ven todos en la primera carga. Tratarla como una nota al pie tiene dos consecuencias medibles: un salto visual desagradable y una métrica de estabilidad visual mala. Tratarla como parte del diseño significa elegir a conciencia qué fuente de sistema se parece más en color de página y en anchura, medirle las métricas, y ajustarlas con los descriptores hasta que el intercambio sea invisible. Cuando lo consigues pasa algo que parece magia: la fuente web entra sin que nada se mueva ni un píxel, y el usuario percibe que el texto “se afina” en lugar de que la página “se recoloca”. Ese es el estándar al que hay que aspirar, y no requiere ninguna característica que no lleve años en los navegadores; requiere aceptar que la fuente de respaldo forma parte del sistema y merece el mismo cuidado que la principal.