font-variation-settings frente a las propiedades de alto nivel
Por qué una lista de ejes no cascadea por partes, qué se rompe con font-weight y con la síntesis, por qué la animación salta, y el patrón correcto para los ejes personalizados.
font-variation-settings puede hacer todo lo que hacen font-weight, font-stretch y font-style, y por eso mucha gente la usa para todo. Es un error, y no un error de estilo: la propiedad de bajo nivel rompe la cascada por eje, rompe la síntesis del navegador y rompe la interpolación. La propia especificación de CSS Fonts la marca como característica de bajo nivel que solo debe usarse cuando no hay alternativa de alto nivel.
- Explicar por qué una lista de ejes no cascadea eje a eje.
- Predecir qué le pasa a un elemento anidado que redefine la lista.
- Reconocer los casos en que la interpolación es discreta y produce un salto.
- Escribir el patrón de composición con custom properties para ejes personalizados.
Una lista no es una cascada
font-weight y font-stretch son propiedades distintas. Cada una cascadea, se hereda y se sobrescribe por su cuenta. Un elemento puede heredar el peso del padre y cambiar solo la anchura.
font-variation-settings es una sola propiedad cuyo valor es una lista completa. No hay cascada por eje porque para el motor no hay ejes: hay un valor.
.tarjeta {
font-variation-settings: 'wght' 700, 'wdth' 100;
}
.tarjeta .subtitulo {
font-variation-settings: 'wdth' 85;
}
El subtítulo no hereda 'wght' 700. Al declarar la propiedad, ha sustituido el valor entero, y el eje de peso vuelve a su valor por defecto en la fuente, que suele ser 400. El resultado es un subtítulo estrecho y ligero cuando lo que se pedía era estrecho y negrita.
Este bug tiene una firma reconocible: funciona hasta que alguien añade una regla más abajo, y entonces desaparece un efecto que estaba en otra parte del archivo. Con las propiedades de alto nivel el mismo código es correcto sin pensarlo:
.tarjeta { font-weight: 700; font-stretch: 100%; }
.tarjeta .subtitulo { font-stretch: 85%; } /* el peso se hereda */
Lo que se rompe con font-weight
El daño va más allá de la herencia, porque hay un ecosistema entero construido sobre el valor computado de font-weight, y font-variation-settings no lo toca.
Los estilos del navegador dejan de funcionar. Los elementos de énfasis fuerte se estilan por defecto con un peso relativo, que se computa a partir del font-weight heredado. Si el peso del párrafo viene de la lista de ejes, el font-weight computado sigue siendo 400, así que el énfasis calcula su peso desde 400 y no desde el peso real que se está viendo. Con suerte queda igual que el texto normal; con menos suerte queda más ligero que su contexto.
Los pesos relativos no se pueden usar. Las palabras clave que suben o bajan un escalón operan sobre el valor computado. Si ese valor no refleja lo que se ve, la operación no significa nada.
La síntesis se confunde. Cuando el navegador cree que el peso pedido es 700 y la cara emparejada es de 400, puede sintetizar una negrita. Si además la lista de ejes ya había puesto 'wght' 700, obtienes una negrita falsa encima de una negrita real: el texto se empasta y los contraformas se cierran. Es la causa más habitual del texto variable que se ve sucio.
El emparejamiento no lo ve. El algoritmo que elige qué cara de @font-face usar mira font-weight, font-style y font-stretch. font-variation-settings se aplica después, sobre la cara ya elegida. Si tienes declaradas varias caras, la lista de ejes no participa en la elección y puedes acabar aplicando 'wght' 700 sobre el archivo equivocado.
Lo que se rompe con la animación
Aquí está la razón más dura y la que menos gente conoce.
font-weight interpola como un número. Una transición de 400 a 700 recorre los valores intermedios y el resultado es suave.
font-variation-settings interpola por eje solo si las dos listas contienen exactamente los mismos ejes en el mismo orden. Si no coinciden, la especificación exige interpolación discreta: el valor salta de uno a otro a mitad de la transición, sin pasos intermedios.
Y ahora el detalle que hace que esto falle casi siempre en la práctica: el valor inicial de la propiedad es normal, que no es una lista. Así que el caso más común de todos —un estado por defecto sin la propiedad y un estado de hover que la declara— es siempre discreto.
/* SALTA: normal no es una lista con los mismos ejes */
.boton {
transition: font-variation-settings 200ms;
}
.boton:hover {
font-variation-settings: 'wght' 700;
}
/* INTERPOLA: mismo eje, mismo orden, en los dos estados */
.boton {
font-variation-settings: 'wght' 400;
transition: font-variation-settings 200ms;
}
.boton:hover {
font-variation-settings: 'wght' 700;
}
/* MEJOR: la propiedad de alto nivel no tiene ninguna de estas trampas */
.boton {
font-weight: 400;
transition: font-weight 200ms;
}
.boton:hover {
font-weight: 700;
}
La segunda versión funciona, y sigue teniendo el problema de la cascada: cualquier regla que quiera añadir otro eje a ese botón tendrá que repetir el peso.
El patrón correcto para los ejes personalizados
Para wght, wdth, slnt, ital y opsz la conclusión es simple: usa siempre la propiedad de alto nivel. Sin excepciones ni matices; no hay nada que la propiedad de bajo nivel haga mejor.
Para los ejes personalizados no hay alternativa, así que la técnica consiste en recuperar la cascada por eje componiendo la lista a partir de custom properties, y recuperar la interpolación registrando esas propiedades con un tipo numérico.
@property --eje-grado {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
@property --eje-casual {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
.tipografia {
font-variation-settings:
'GRAD' var(--eje-grado),
'CASL' var(--eje-casual);
transition: --eje-grado 150ms ease-out, --eje-casual 300ms ease;
}
.tipografia .destacado { --eje-grado: 120; } /* solo toca su eje */
.tipografia:hover { --eje-casual: 1; } /* y este el suyo */
Ahora cada eje se sobrescribe por separado, se hereda por separado y se anima por separado, que es exactamente lo que dan las propiedades de alto nivel. La lista completa se escribe una sola vez en el sitio donde se establece la tipografía, y nadie más vuelve a tocar font-variation-settings.
La transición sobre las custom properties registradas funciona porque el tipo <number> es interpolable; sin el registro con @property serían cadenas y volverías a tener un salto.
Si un elemento tiene font-weight: 700 y además font-variation-settings: 'wght' 400, gana la lista de ejes, porque se aplica después sobre la instancia. El resultado es un texto que se ve ligero mientras el font-weight computado dice 700, con lo que la síntesis, los pesos relativos y los estilos del navegador razonan sobre un valor que no corresponde a lo que hay en pantalla. Es el estado más confuso posible de depurar, porque el panel de estilos muestra las dos declaraciones aplicadas y ninguna tachada.
font-variation-settings no es un caso aislado: es un miembro de una familia entera de propiedades de CSS que empaquetan varios ajustes independientes en un único valor de lista. font-feature-settings hace lo mismo con las características de OpenType, transform con las transformaciones —que por eso ganaron sus longhands independientes—, filter con los filtros, box-shadow con las sombras. Todas comparten exactamente el mismo defecto y el mismo síntoma: puedes componer, pero no puedes sobrescribir una parte, así que cualquier regla que quiera cambiar un elemento de la lista tiene que reescribir la lista entera, incluidos los valores que le llegaban de otro sitio y que ni siquiera conoce. Cuando el CSS crece, eso se convierte en duplicación forzosa, y la duplicación forzosa se convierte en desincronización. La lección general es que el poder expresivo de una lista y la componibilidad de la cascada son incompatibles, y que cada vez que el lenguaje ha podido, ha migrado de la lista a las propiedades sueltas: rotate, scale y translate existen exactamente por esto. Cuando te toque una bolsa de ajustes sin longhands, el patrón de composición con custom properties registradas es la mejor reconstrucción disponible de lo que perdiste, y conviene aplicarlo desde el primer día y no cuando el problema ya haya aparecido, porque a esas alturas la lista ya está escrita en quince sitios.