Animar lo inanimable: ángulos, gradientes y colores
Por qué una custom property sin registrar salta en vez de interpolar, cómo animar un gradiente cónico o una parada de color, cómo elegir el camino de interpolación de un color y qué cuesta todo esto.
Un gradiente no es animable. Nunca lo ha sido: background-image interpola de forma discreta y lo único que consigues al ponerlo en una transición es un salto brusco a mitad de camino. Registrar una custom property no cambia eso, y sin embargo permite animar gradientes, y bordes que giran, y barras que se llenan. El truco es que no animas el gradiente: animas un número, y el gradiente se recalcula a partir de él.
- Explicar por qué una custom property sin registrar solo interpola de forma discreta.
- Animar un ángulo para producir un gradiente cónico en movimiento.
- Controlar el camino de interpolación de un color animando su matiz.
- Medir y acotar el coste de animar una propiedad registrada.
Por qué antes saltaba
El valor calculado de una custom property sin registrar es el flujo de tokens tal como lo escribiste. Interpolar significa calcular un valor intermedio entre dos, y entre dos flujos de tokens no hay ningún intermedio definido: 10px y 40px son, para el motor, dos cadenas sin estructura. Por eso una custom property no registrada se interpola de forma discreta: se queda en el valor inicial hasta la mitad de la transición y salta al final.
/* Sin registrar: esto salta a mitad de camino, no crece. */
.caja { --tamano: 10px; block-size: var(--tamano); transition: --tamano 500ms; }
.caja:hover { --tamano: 40px; }
Al registrar como <length>, el valor calculado deja de ser una cadena y pasa a ser una longitud resuelta en píxeles. Ya hay estructura, ya hay aritmética, ya hay interpolación:
@property --tamano {
syntax: "<length>";
inherits: false;
initial-value: 10px;
}
/* Ahora la misma transicion crece suavemente. */
Esa es toda la magia. Lo que se anima nunca es el gradiente ni la sombra: es un escalar tipado, y la apariencia se recalcula en cada fotograma a partir de él.
Ángulos y gradientes
El caso más vistoso y el más útil. Un borde que gira, imposible antes sin canvas o sin SVG:
@property --angulo {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes girar {
to { --angulo: 360deg; }
}
.borde-vivo {
position: relative;
border-radius: 0.75rem;
padding: 1px;
background: conic-gradient(
from var(--angulo),
oklch(70% 0.2 20),
oklch(70% 0.2 200),
oklch(70% 0.2 20)
);
animation: girar 4s linear infinite;
}
.borde-vivo > * {
border-radius: calc(0.75rem - 1px);
background: canvas;
}
@media (prefers-reduced-motion: reduce) {
.borde-vivo { animation: none; }
}
Y la barra de progreso que se llena de verdad, con una sola parada animada y un corte duro:
@property --avance {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.barra {
--avance: 0%;
block-size: 0.5rem;
border-radius: 999px;
background: linear-gradient(
to right,
oklch(65% 0.18 145) var(--avance),
oklch(92% 0.01 260) 0
);
transition: --avance 400ms ease-out;
}
El 0 de la segunda parada no es un error: en un gradiente, una parada con posición menor que la anterior se ajusta a la de la anterior, y eso produce un corte limpio sin necesidad de repetir el valor de --avance.
Escribir .barra { --avance: 72%; } desde una regla, o desde el atributo style generado por el servidor, anima la barra sin una línea de JavaScript.
Colores por el camino que tú elijas
Un <color> registrado se interpola con las mismas reglas que cualquier propiedad de color, y eso está bien para transiciones cortas. Cuando quieres control sobre el camino —que un color recorra la rueda de matices en lugar de atravesar el gris por el centro— la técnica es no animar el color, sino animar su matiz y construir el color con él:
@property --matiz {
syntax: "<angle>";
inherits: false;
initial-value: 260deg;
}
.pastilla {
background: oklch(70% 0.16 var(--matiz));
transition: --matiz 600ms ease-in-out;
}
.pastilla:hover { --matiz: 20deg; }
Al interpolar el ángulo, el color recorre matices intermedios manteniendo constantes la claridad y el croma. Ninguna interpolación directa entre dos colores hace eso, porque interpola las tres coordenadas a la vez. Es la diferencia entre un degradado que pasa por naranjas y amarillos y otro que pasa por un gris sucio.
El mismo principio sirve para cualquier apariencia que dependa de varios números coordinados. Registra los escalares, anima los escalares, y deja que las declaraciones compongan.
@property --p { syntax: "<number>"; inherits: false; initial-value: 0; }
.aparecer {
opacity: var(--p);
translate: 0 calc((1 - var(--p)) * 1rem);
filter: blur(calc((1 - var(--p)) * 4px));
transition: --p 320ms ease-out;
}
.aparecer.visible { --p: 1; }
Un solo valor animado gobierna tres propiedades a la vez, cada una con su propia relación con el progreso. Sin custom properties registradas, esto son tres transiciones que hay que mantener sincronizadas a mano.
Lo que cuesta animar una propiedad
Aquí toca la parte honesta. Animar una custom property no se ejecuta en el compositor. Cada fotograma implica recalcular el valor, recalcular las declaraciones que dependen de él y repintar. Comparado con animar transform u opacity, que el compositor resuelve sin tocar el hilo principal, es caro.
Tres reglas para que no se note.
Registra como no heredable todo lo que animes. Con inherits: false, el cambio invalida solo el elemento donde ocurre. Con inherits: true invalida su subárbol entero, y si la animación está en :root, invalida el documento en cada fotograma.
No lo uses para lo que ya sabe hacer el compositor. Si lo que quieres es mover, escalar o desvanecer, usa translate, scale y opacity directamente. La animación paramétrica es para lo que de otro modo sería imposible: gradientes, formas, recortes, sombras compuestas.
Cuenta los elementos. Un borde giratorio en una tarjeta destacada es gratis. El mismo borde en cuarenta tarjetas de una rejilla es un problema medible. Si necesitas el efecto en muchos elementos, mira si puedes moverlo a una única capa con transform.
Estas animaciones son llamativas por definición, y varias de ellas —bordes que giran, gradientes que se desplazan— entran de lleno en lo que la preferencia de movimiento reducido pretende evitar. Envuelve la animación en @media (prefers-reduced-motion: no-preference) o desactívala explícitamente, como en el ejemplo del borde. Que sea CSS puro no la hace inofensiva.
El cambio profundo que trae @property no es la lista de cosas nuevas que se pueden animar: es que cambia qué se anima. En el modelo clásico, animas la propiedad visual directamente, y por tanto necesitas una pista de animación por cada propiedad, cada una con su propia curva, y coordinarlas es tu problema. Con propiedades registradas puedes animar un parámetro —un número, un ángulo, un porcentaje que representa el progreso de algo— y escribir la apariencia como una función pura de ese parámetro repartida por tantas declaraciones como haga falta. Una sola pista dirige cinco propiedades, cada una con su relación matemática con el progreso, y la sincronización deja de existir como problema porque nunca hubo dos cosas que sincronizar. Es exactamente el modelo con el que se piensa una animación en cualquier motor gráfico serio, y la razón por la que tanto efecto no trivial acababa en JavaScript no era que CSS no supiera interpolar, sino que no había forma de expresar “todo esto depende de la misma variable”. Ahora la hay, y con ella se mueve a CSS una familia entera de efectos que vivían en un requestAnimationFrame. El contrapunto honesto es que esa expresividad se paga en el hilo principal: la interpolación de una custom property no se puede delegar al compositor, porque el compositor no sabe qué declaraciones dependen del valor ni cómo recalcularlas. Así que la regla de ingeniería que sale de aquí es de reparto: lo que el compositor sabe hacer, déjaselo al compositor; lo que solo se puede expresar como función de un parámetro, hazlo con una propiedad registrada; y no mezcles ambas cosas en el mismo efecto sin medir. Quien aplica esa regla obtiene efectos que antes exigían código y siguen yendo a sesenta fotogramas; quien la ignora acaba con un borde precioso que hace tartamudear el desplazamiento de la página.
- Comprueba que una custom property sin registrar salta a mitad de la transición.
- Regístrala como
<length>y verifica que ahora interpola. - Monta el borde con gradiente cónico giratorio y desactívalo con movimiento reducido.
- Anima el matiz de un color en OKLCH y compáralo con animar el color directamente.
- Dirige tres propiedades desde un único parámetro de progreso y mide el coste por fotograma.