wandres.dev
ANIMAR LA ENTRADA · @starting-style y transition-behavior

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.

⏱ 17 min

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.

🎯 Al terminar esta lección sabrás
  • 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 display y content-visibility y 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 no es discreta del todo, y esa rareza es lo que hace funcionar el patrón portable de siempre

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.

⚔️ Observa los saltos
  1. Anima background-image entre dos URLs con ease, con step-start y con step-end. Anota en qué instante salta cada una.
  2. Comprueba con el muestreo de esta lección en qué momento cambia display al abrir y al cerrar un panel con allow-discrete.
  3. Monta el patrón portable con visibility y comprueba con el tabulador que el elemento deja de ser enfocable justo al terminar la transición y no antes.
  4. Sustituye visibility: hidden por opacity: 0 a secas y comprueba qué pasa con el foco.