Las tres vías para animar SVG
CSS, SMIL y el DOM: qué puede hacer cada una, qué no puede hacer ninguna otra, y el criterio para elegir sin arrepentirse.
SVG es el único formato de la plataforma web que tiene su propio sistema de animación declarativo, heredado de una especificación anterior a CSS, y que además participa en CSS y en el DOM como cualquier otro elemento. El resultado es que hay tres formas de animar lo mismo, con solapes parciales y con huecos que solo una de ellas cubre. Elegir bien no es cuestión de gusto: cada vía tiene un territorio exclusivo y saber cuál es evita reescribir el trabajo.
- Enumerar las tres vías y el mecanismo de cada una.
- Identificar el territorio exclusivo de cada vía.
- Aplicar el criterio de elección a un caso concreto.
- Anticipar los problemas de mezclar dos vías sobre el mismo atributo.
Las tres
CSS. Muchos atributos de SVG son, además de atributos, propiedades de CSS. fill, stroke, opacity, stroke-dasharray y una lista larga son atributos de presentación, y desde SVG 2 también lo son las propiedades de geometría: cx, cy, r, x, y, width, height. Todo lo que sea una propiedad de CSS se anima con @keyframes y con transiciones exactamente igual que en HTML, con las mismas garantías de rendimiento y la misma integración con prefers-reduced-motion.
SMIL. Los elementos <animate>, <animateTransform>, <animateMotion> y <set> se escriben dentro del SVG, como hijos del elemento que animan o apuntando a él por identificador. Son declarativos, viven en el marcado y no necesitan ni CSS ni JavaScript. Su alcance es cualquier atributo animable de SVG, sea o no una propiedad de CSS.
El DOM. Modificar atributos desde JavaScript, con setAttribute o con las interfaces específicas de SVG, o usar Web Animations sobre las propiedades que sean de CSS. Es la vía imperativa y la única que puede calcular valores.
El territorio exclusivo de cada una
Lo que hace que la elección importe es que las tres no cubren lo mismo. Estos son los huecos.
Solo CSS puede llevar la animación al compositor. Una animación de transform u opacity sobre un elemento SVG desde CSS se beneficia de la misma ruta rápida que en HTML. SMIL y las escrituras de atributos desde JavaScript ocurren en el hilo principal, siempre.
Solo CSS se integra con el resto del sistema. Media queries, prefers-reduced-motion, custom properties, @supports, capas de cascada, getAnimations(). Una animación SMIL no aparece en getAnimations() y no hay forma de apagarla desde una media query.
Solo SMIL puede animar atributos que no son propiedades de CSS. Es su territorio irreductible y no es pequeño: points de un polígono, viewBox del propio SVG, x1 e y1 de una línea, el offset de una parada de degradado. Ninguno de esos es una propiedad de CSS y por tanto ninguno se puede animar con @keyframes.
Solo SMIL tiene temporización declarativa basada en eventos. El atributo begin acepta cosas como click, otraAnimacion.end + 1s o mouseover, y con eso se construyen secuencias y reacciones sin una línea de JavaScript. CSS no tiene nada equivalente: sus animaciones arrancan cuando el elemento coincide con un selector, y encadenar requiere retardos calculados a mano.
Solo el DOM puede calcular. Si el valor depende de datos, de una medida tomada en tiempo de ejecución o de la interacción, hay que pasar por JavaScript. El caso más habitual es el de la lección sobre el trazo: la longitud de un trazado solo se conoce preguntándosela al elemento.
Aquí está el modelo mental que evita la mayoría de los desconciertos con SVG y CSS. Cuando escribes fill="red" en el marcado, no estás poniendo un valor: estás declarando una regla de CSS. Los atributos de presentación de SVG se comportan como si fueran declaraciones insertadas al principio de la hoja de estilos de autor con especificidad cero. De ahí salen tres consecuencias que explican casi todos los casos raros. Primera: cualquier selector de CSS gana al atributo, siempre. Un svg path { fill: blue } sobrevive a un fill="red" en el marcado, incluso siendo un selector débil, porque el atributo tiene especificidad cero. Mucha gente descubre esto al revés, intentando entender por qué su marcado “no se aplica”. Segunda: el atributo sí gana a una hoja de estilos del usuario o del navegador, porque está en el origen de autor. Tercera, y la que de verdad muerde: si animas con CSS un atributo que también estás modificando desde JavaScript con setAttribute, la animación gana siempre, porque las animaciones se resuelven por encima de todas las declaraciones normales de la cascada. El síntoma es un setAttribute que “no hace nada” y que en realidad funciona perfectamente: está escribiendo el atributo y la animación lo está pisando en el cálculo de estilo. Para modificar desde JavaScript algo que está animado por CSS hay que tocar el estilo en línea con !important, o mejor, dejar de mezclar las dos vías sobre la misma propiedad.
El criterio de elección
Con los territorios claros, la decisión se toma con tres preguntas encadenadas.
¿Lo que quieres animar es una propiedad de CSS? Si sí, usa CSS. Sin excepciones. Es la vía con mejor rendimiento, mejor integración y menos código.
Si no lo es, ¿el valor se puede calcular de antemano? Si sí, y no necesitas temporización por eventos, SMIL es la opción declarativa y no requiere JavaScript. Si necesitas temporización por eventos, SMIL es directamente la mejor opción, porque es lo único que la tiene.
Si el valor depende de datos o de medidas, usa el DOM. Y dentro del DOM, prefiere Web Animations sobre propiedades de CSS a escribir atributos en cada frame, siempre que la propiedad exista.
La tabla resumida, con las trampas señaladas:
| Lo que quieres animar | Vía | Aviso |
|---|---|---|
fill, stroke, opacity |
CSS | — |
stroke-dasharray, stroke-dashoffset |
CSS | La longitud suele venir del DOM |
cx, cy, r, x, y, width, height |
CSS | Propiedades de geometría, interoperables |
transform |
CSS | Ojo con transform-box, tiene lección propia |
d de un trazado |
CSS | No funciona en Safari |
points de un polígono |
SMIL | No es propiedad de CSS |
viewBox |
SMIL o DOM | No es propiedad de CSS |
| Recorrido por un trazado | CSS con offset-path, o SMIL |
— |
| Cualquier cosa que dependa de datos | DOM | — |
Esa fila del d merece una nota, porque es el caso donde más gente se estrella: la propiedad de CSS d existe en Chromium desde la versión 52 y en Firefox desde la 97, y Safari no la implementa. La propiedad se parsea sin error y no tiene ningún efecto, que es el peor modo de fallo posible porque no hay nada que detectar. Cualquier morphing de trazados con CSS puro está fuera de la mesa mientras eso siga así.
Mezclar vías sobre el mismo atributo
La última advertencia, y es la que produce los bugs más difíciles de localizar. Si dos vías animan el mismo atributo a la vez, el resultado depende de un orden de precedencia que no está documentado en ningún sitio evidente y que no siempre es intuitivo.
La regla general es que una animación SMIL sobre un atributo que también es propiedad de CSS se comporta como una declaración por encima de las normales pero por debajo de las animaciones de CSS. En la práctica: si animas fill con SMIL y con @keyframes a la vez, gana CSS y tu SMIL parece no existir.
La conclusión práctica es sencilla y hay que aplicarla con disciplina: un atributo, una vía. Si un icono tiene una animación de trazo con CSS y una de forma con SMIL, que sean atributos distintos. Y si estás heredando un SVG exportado de una herramienta de diseño que ya trae elementos <animate> dentro, lo primero es decidir si te quedas con ellos o los borras, no añadir CSS encima a ver qué pasa.