Propiedades discretas y la regla del salto
Qué hace exactamente el motor con un valor que no se puede interpolar, por qué el salto ocurre a mitad de camino, y las dos excepciones que lo cambian todo.
Una propiedad discreta es la que no tiene valores intermedios: display no puede estar medio en none, position no puede estar entre absolute y fixed. El comportamiento por defecto para esos casos —saltar del primer valor al segundo justo a la mitad de la duración— es una convención razonable y produce un resultado inservible en el caso más importante. Por eso hay dos propiedades con una regla propia, y entender esa asimetría es lo que hace que el patrón de entrada y salida sin JavaScript funcione.
- Definir qué hace discreta a una propiedad y enumerar las principales.
- Explicar la regla del salto al 50 por ciento y de dónde sale.
- Describir la regla especial de
displayycontent-visibilityy por qué es distinta. - Reconocer el comportamiento particular de
visibility.
Qué hace discreta a una propiedad
Como vimos en la lección sobre qué valores interpolan, la interpolación requiere que los dos valores sean del mismo tipo computado y que ese tipo tenga definida una operación de mezcla. Cuando falta cualquiera de las dos, el comportamiento es discreto.
Las propiedades discretas más relevantes en la práctica:
| Propiedad | Por qué es discreta |
|---|---|
display |
Elección entre modos de layout, sin punto medio |
position |
Elección entre esquemas de posicionamiento |
visibility |
Elección binaria, con una excepción propia |
content-visibility |
Elección entre renderizar o saltarse el contenido |
overlay |
Elección entre estar o no en la capa superior |
float, clear |
Elección entre alternativas |
flex-direction, flex-wrap |
Elección entre alternativas |
background-image cuando es una URL |
Dos imágenes no tienen mezcla definida |
font-family |
Dos familias no tienen mezcla definida |
mix-blend-mode |
Elección entre modos de fusión |
| Custom properties sin registrar | No tienen tipo |
La regla del salto al 50 por ciento
Cuando una propiedad discreta participa en una transición o en una animación, el valor salta del primero al segundo cuando el progreso alcanza el 50 por ciento.
La razón de que sea a mitad y no al principio o al final es de simetría: si saltara al principio, invertir la animación cambiaría el resultado; si saltara al final, lo mismo con signo contrario. Al 50 por ciento, la operación es simétrica y reversible, que es lo que se espera de una interpolación.
Es importante entender que el progreso al que se aplica el salto es el progreso transformado, es decir el que sale de la función de suavizado. Con una curva de salida pronunciada, el progreso alcanza 0.5 muy pronto en el tiempo, así que el salto ocurre antes de la mitad de la duración. Con steps(1, jump-end) no ocurre hasta el final, y con steps(1, jump-start) ocurre inmediatamente. Eso da control fino sobre el instante del salto sin salir de CSS.
/* El cambio de background-image ocurre al 50% del progreso. */
.a { transition: background-image 400ms; }
/* Ocurre al final: la curva no llega a 0.5 hasta el ultimo instante. */
.b { transition: background-image 400ms step-end; }
/* Ocurre inmediatamente. */
.c { transition: background-image 400ms step-start; }
Ese control es útil con mix-blend-mode, con font-family y con cualquier cambio discreto que quieras sincronizar con otra cosa que sí interpola.
Las dos excepciones: display y content-visibility
Aplicar la regla del 50 por ciento a display produce exactamente lo que no quieres. Imagina un panel que se cierra pasando de block a none con un desvanecido de 300 milisegundos: al llegar a los 150, display saltaría a none, el elemento dejaría de renderizarse, y la segunda mitad del desvanecido no se vería. El panel desaparecería de golpe a media opacidad.
Por eso la especificación define un comportamiento propio para estas dos propiedades, y merece la pena enunciarlo con precisión porque no es simétrico:
- Al ir hacia el valor que hace visible el contenido, el salto ocurre al 0 por ciento: el elemento se vuelve visible inmediatamente, al principio de la transición.
- Al ir hacia el valor que lo oculta, el salto ocurre al 100 por ciento: el elemento sigue visible hasta el último instante.
En las dos direcciones, el resultado es el mismo: el elemento se renderiza durante toda la transición. Que es exactamente lo que hace falta para que la animación se vea completa.
| Transición | display a lo largo del tiempo |
|---|---|
none a block |
block desde el 0 por ciento |
block a none |
block hasta el 100 por ciento |
content-visibility sigue la misma lógica con hidden y visible, y sirve para el mismo propósito en listas donde quieres saltarte el renderizado del contenido fuera de pantalla sin perder la posibilidad de animar su aparición.
Esta regla especial no se activa sola: hace falta habilitar explícitamente el comportamiento discreto con transition-behavior: allow-discrete, que es el tema de la lección siguiente.
visibility está en la lista de propiedades discretas y tiene una interpolación definida que no es la de las discretas normales, y esa peculiaridad —que lleva en la plataforma desde mucho antes que todo lo demás de este nivel— es la razón de que el patrón clásico de ocultar con desvanecido funcione en cualquier navegador desde hace más de una década. La regla es: si alguno de los dos extremos es visible, el valor visible se usa durante toda la interpolación, y solo en el instante final se aplica el valor de destino. Es decir, visibility se comporta exactamente como se comportan hoy display y content-visibility con allow-discrete, pero sin necesitar ninguna palabra clave y sin necesitar que se inventara nada. Eso convierte el par de opacity más visibility en la solución portable de referencia para ocultar algo con animación: el elemento sigue siendo visible mientras la opacidad baja, y solo cuando la transición termina desaparece del árbol de accesibilidad y deja de ser enfocable. Hay dos cosas que ese patrón no da y que hay que saber para no confundirlo con la solución completa. La primera es que visibility: hidden no saca el elemento del flujo: sigue ocupando su espacio, y para un menú o un panel eso suele ser inaceptable. La segunda es que el elemento sigue teniendo cajas de layout y sigue participando en el cálculo de tamaño de sus contenedores, así que el coste no desaparece. Con display: none se van las dos cosas, y por eso hacía falta la maquinaria nueva. Pero si lo que necesitas es simplemente que algo se oculte con animación sin salir del flujo, visibility sigue siendo la respuesta correcta, la más portable y la que menos piezas requiere.
/* Patron portable, sin allow-discrete y sin @starting-style. */
.aviso {
opacity: 0;
visibility: hidden;
transition: opacity 200ms ease-out, visibility 200ms;
}
.aviso[data-visible] {
opacity: 1;
visibility: visible;
}
Cómo comprobar el comportamiento discreto
La forma más directa de ver el salto es animar una custom property registrada con una sintaxis discreta y leerla. Pero para el caso real, lo que interesa es comprobar en qué instante ocurre el salto de display, y eso se ve mejor con un registro de eventos:
// Registra el instante de cada cambio observable durante la transicion.
const panel = document.querySelector('.panel');
const t0 = performance.now();
const obs = new MutationObserver(() => {
console.log(
Math.round(performance.now() - t0),
'ms display =', getComputedStyle(panel).display,
' opacity =', getComputedStyle(panel).opacity
);
});
// El cambio de display no muta atributos, asi que hay que muestrear.
const id = setInterval(() => {
const s = getComputedStyle(panel);
console.log(Math.round(performance.now() - t0), s.display, s.opacity);
}, 50);
setTimeout(() => { clearInterval(id); obs.disconnect(); }, 600);
Ese muestreo enseña la asimetría de forma inmediata: al abrir, display vale el valor visible desde el primer muestreo; al cerrar, lo mantiene hasta el último.
- Anima
background-imageentre dos URLs conease, constep-starty constep-end. Anota en qué instante salta cada una. - Comprueba con el muestreo de esta lección en qué momento cambia
displayal abrir y al cerrar un panel conallow-discrete. - Monta el patrón portable con
visibilityy comprueba con el tabulador que el elemento deja de ser enfocable justo al terminar la transición y no antes. - Sustituye
visibility: hiddenporopacity: 0a secas y comprueba qué pasa con el foco.