wandres.dev
TRANSITIONS · La animación de estado

Qué valores son interpolables y por qué

El criterio que decide si dos valores se pueden mezclar, los tipos que interpolan, los que no, y cómo convertir una custom property en interpolable.

⏱ 19 min

La pregunta de si una propiedad “se puede animar” está mal formulada. Lo que se interpola no son propiedades sino valores, y la respuesta depende del tipo del valor computado y de si los dos extremos son del mismo tipo. Por eso background-image a veces interpola y a veces no, por eso transform interpola solo si las dos listas de funciones son compatibles, y por eso una custom property salta a menos que le digas al motor de qué tipo es.

🎯 Al terminar esta lección sabrás
  • Enunciar el criterio general que decide si dos valores se pueden interpolar.
  • Clasificar los tipos de valor computado en interpolables y discretos.
  • Explicar por qué dos listas de transformaciones pueden no interpolar y qué hace el motor entonces.
  • Convertir una custom property en interpolable con @property.

El criterio general

Para interpolar dos valores, el motor necesita una función que dada una fracción entre 0 y 1 produzca un valor intermedio. Esa función existe cuando los dos valores son del mismo tipo computado y ese tipo tiene una operación de mezcla definida. Cuando falta cualquiera de las dos condiciones, el comportamiento es discreto: se salta del primero al segundo a mitad de camino.

Eso desmonta la lista mental de “propiedades animables” y la sustituye por una pregunta sobre valores. width interpola de 10px a 200px porque ambos son longitudes. No interpola de 10px a auto porque auto no es una longitud, y ese caso concreto tiene una lección entera en el nivel 7. background-image no interpola de una URL a otra porque una URL no tiene puntos intermedios, y sí interpola de un degradado a otro si los dos degradados son del mismo tipo y tienen el mismo número de paradas.

Los tipos que interpolan

Tipo computado Cómo interpola Ejemplos
Número Aritmética directa opacity, flex-grow
Entero Aritmética y redondeo z-index, column-count
Longitud Aritmética sobre el valor en píxeles width, margin, border-width
Porcentaje Aritmética sobre el porcentaje left, background-position
Longitud o porcentaje Se combina en un calc() inset, translate
Color Por componentes, en un espacio de color color, background-color
Lista de transformaciones Por función, si son compatibles transform
Lista de sombras Por sombra, si hay el mismo número box-shadow, text-shadow
Lista de filtros Por filtro, si la secuencia es compatible filter
Imagen de degradado Por parada, si el tipo y el número coinciden background-image
Ruta de recorte Por vértice, si el número coincide clip-path

Dos observaciones sobre esa tabla. La primera: casi todas las listas exigen compatibilidad estructural. Dos sombras interpolan si hay el mismo número de sombras; si una regla declara dos y la otra tres, el comportamiento es discreto. Lo mismo con filtros y con degradados. La forma de arreglarlo es siempre la misma: declarar en los dos extremos el mismo número de elementos, poniendo los que sobran con valores neutros.

/* Discreto: numeros de sombras distintos. */
.a { box-shadow: 0 1px 2px rgb(0 0 0 / .2); }
.a:hover { box-shadow: 0 1px 2px rgb(0 0 0 / .2), 0 8px 24px rgb(0 0 0 / .3); }

/* Interpola: dos sombras en ambos estados, la segunda neutra al principio. */
.b { box-shadow: 0 1px 2px rgb(0 0 0 / .2), 0 0 0 rgb(0 0 0 / 0); }
.b:hover { box-shadow: 0 1px 2px rgb(0 0 0 / .2), 0 8px 24px rgb(0 0 0 / .3); }

La segunda: el color interpola en un espacio de color, y cuál sea cambia el resultado. Interpolar de rojo a verde pasando por sRGB produce un marrón apagado en el punto medio; hacerlo en OKLCH produce un amarillo. Desde CSS Color 4 se puede elegir con la sintaxis de interpolación en degradados y con color-mix(), y las transiciones usan el espacio que el motor considere apropiado para el par de valores. Cuando el color importa, el control fino se consigue interpolando una custom property numérica y construyendo el color con oklch().

Las transformaciones y su compatibilidad

transform merece un párrafo aparte porque su regla de interpolación es la más elaborada y la que más sorprende.

Si las dos listas tienen la misma secuencia de funciones, el motor interpola función a función. translate(0, 0) rotate(0deg) a translate(100px, 0) rotate(45deg) interpola limpiamente.

Si las secuencias difieren pero una es un prefijo de la otra, el motor rellena la que falta con valores neutros y sigue interpolando función a función.

Si las secuencias son incompatibles —distinto orden, distintas funciones—, el motor no se rinde: convierte las dos listas a matrices y las interpola por descomposición. Descompone cada matriz en traslación, rotación, escala, sesgado y perspectiva, interpola esas componentes y recompone. El resultado es visualmente correcto en la mayoría de los casos y produce trayectorias raras en algunos, típicamente cuando hay rotaciones grandes o escalas negativas.

La consecuencia práctica es que la interpolación de transformaciones nunca es discreta, pero puede ser distinta de lo que esperabas. La forma de tener control total es usar las propiedades independientes translate, rotate y scale, que se interpolan cada una por su cuenta y no pueden entrar en el camino de la descomposición.

/* Trayectoria por descomposicion de matrices: puede sorprender. */
.a { transform: rotate(0deg) translateX(0); }
.a:hover { transform: translateX(100px) scale(1.2); }

/* Cada propiedad interpola por su cuenta: predecible. */
.b { rotate: 0deg; translate: 0; scale: 1; }
.b:hover { translate: 100px; scale: 1.2; }
Una custom property no interpola: salta. Y el aviso es que nunca lo hará por sí sola

Este es el caso que más tiempo cuesta descubrir porque el CSS parece impecable y el resultado es un salto brusco a mitad de la transición. Una custom property declarada de la forma normal —--tono: 250;— tiene un tipo computado que es, literalmente, una secuencia de componentes sin analizar. El motor no sabe si eso es un número, un color, una longitud o el nombre de tu perro; solo sabe que es texto que sustituirá donde aparezca var(--tono). Y sin tipo no hay operación de mezcla, así que la interpolación es discreta: el valor salta del inicial al final en el punto medio. Todo el aparato de transición funciona correctamente —hay una transición, dura lo que dijiste, emite sus eventos— y aun así el resultado es un salto, lo que hace que la gente busque el problema en la duración o en la curva. La solución existe y es @property, que registra la propiedad con una sintaxis: a partir de ahí el motor conoce el tipo, valida el valor, y puede interpolar. Pero hay un segundo efecto de @property que es igual de importante y del que casi nadie habla: te obliga a decidir si la propiedad se hereda. Con inherits: false recortas la invalidación de estilo a un único elemento, y como vimos en el nivel del pipeline, esa diferencia puede ser de dos órdenes de magnitud en el coste de recálculo. Es decir, el registro que necesitabas para que la animación se vea bien es también el que hace que sea barata. Si vas a animar una custom property, regístrala siempre; y si no la vas a animar, regístrala igualmente para poder decidir la herencia.

El registro completo, con las tres declaraciones que exige:

@property --tono {
  syntax: '<number>';
  inherits: false;
  initial-value: 250;
}

@property --sombra-alfa {
  syntax: '<number>';
  inherits: false;
  initial-value: 0.2;
}

.tarjeta {
  --tono: 250;
  --sombra-alfa: 0.2;
  background: oklch(70% 0.14 var(--tono));
  box-shadow: 0 8px 24px rgb(0 0 0 / var(--sombra-alfa));
  transition: --tono 400ms ease-out, --sombra-alfa 400ms ease-out;
}

.tarjeta:hover {
  --tono: 320;
  --sombra-alfa: 0.4;
}

Fíjate en que transition lista las custom properties por su nombre. Eso funciona y es la forma correcta. Y observa el patrón que habilita: se interpola un número, y el color se construye a partir de él en cada fotograma. Eso da control exacto sobre la trayectoria del color, cosa que interpolar directamente el color no permite.

También se puede registrar desde JavaScript, útil cuando la sintaxis se genera:

CSS.registerProperty({
  name: '--tono',
  syntax: '<number>',
  inherits: false,
  initialValue: '250',
});

Los tipos que no interpolan

La lista de tipos discretos es corta y previsible una vez que tienes el criterio: son los valores que no tienen puntos intermedios concebibles.

display, position, float, flex-direction, text-transform, font-family, visibility en parte, background-image cuando es una URL, overflow, white-space. Todo lo que sea una elección entre alternativas, no un punto en un rango.

visibility es la excepción interesante: es discreta, pero su interpolación está definida de forma especial para que el valor visible se use durante toda la transición si alguno de los dos extremos es visible. Eso permite el patrón clásico de desvanecer y ocultar, en el que el elemento sigue siendo visible mientras la opacidad baja y solo desaparece al final.

.aviso {
  opacity: 0;
  visibility: hidden;
  transition: opacity 200ms, visibility 200ms;
}
.aviso[data-visible] {
  opacity: 1;
  visibility: visible;
}

Ese patrón funciona en los dos sentidos sin JavaScript y sin allow-discrete, y es la solución portable para ocultar sin quitar del flujo. Para quitar del flujo hace falta display, y eso pertenece al nivel 6.

⚔️ Encuentra los saltos
  1. Anima box-shadow entre un estado con una sombra y otro con dos. Confirma el salto y arréglalo con la sombra neutra.
  2. Anima una custom property sin registrar y observa el salto a mitad. Regístrala con @property y comprueba la diferencia.
  3. Anima transform entre dos listas con funciones en distinto orden y describe la trayectoria. Reescríbelo con translate y scale y compara.
  4. Comprueba con getComputedStyle qué valor tiene la custom property en el punto medio en las dos versiones.