wandres.dev
ANIMAR SVG · CSS, SMIL y el DOM

SMIL hoy: el estado real

La historia de la deprecación que no ocurrió, qué sigue haciendo SMIL que nada más hace, y las tres razones para no usarlo cuando hay alternativa.

⏱ 17 min

Sobre SMIL circula una afirmación repetida durante una década y que hoy es falsa: que está deprecado. La historia real es más interesante que el rumor, y su desenlace deja un sistema perfectamente vivo, soportado en los tres motores, que sigue siendo la única forma de hacer algunas cosas y una mala forma de hacer casi todas las demás. Esta lección cierra el nivel separando las dos categorías con datos.

🎯 Al terminar esta lección sabrás
  • Enunciar el estado real de soporte y de deprecación de SMIL.
  • Escribir los cuatro elementos de animación de SMIL y su temporización.
  • Identificar los tres casos donde SMIL sigue siendo la única opción.
  • Justificar por qué no es la opción por defecto.

La deprecación que se retiró

En 2015, el equipo de Blink publicó una intención de deprecar SMIL en Chromium, con el argumento de que CSS y Web Animations lo cubrirían y de que mantener dos sistemas de animación era caro. La reacción de la comunidad fue contundente, con dos casos concretos que en aquel momento CSS no podía cubrir: la interpolación de trazados y la animación dentro de un SVG referenciado desde una etiqueta de imagen. En 2016 el equipo suspendió la deprecación, y ahí sigue.

El estado hoy, según los datos de compatibilidad de referencia, es inequívoco: los elementos animate, animateTransform, animateMotion y set están marcados como no deprecados, en vía estándar, y soportados en Chromium, Firefox y Safari. animate funciona en los tres motores desde hace más de una década.

Lo único que sí desapareció es <animateColor>, que quedó obsoleto porque <animate> con attributeName="fill" hace lo mismo. Si te lo encuentras en un SVG antiguo, se puede sustituir sin pérdida.

Así que la frase correcta no es “SMIL está deprecado” sino: SMIL está vivo, funciona en todas partes y no es la mejor herramienta para la mayoría de los casos. Que son dos afirmaciones muy distintas.

Los cuatro elementos

<animate> interpola un atributo entre valores:

<svg viewBox="0 0 100 40">
  <polygon points="10,30 30,10 50,30">
    <animate attributeName="points"
             values="10,30 30,10 50,30; 10,10 30,30 50,10; 10,30 30,10 50,30"
             dur="2s" repeatCount="indefinite" />
  </polygon>
</svg>

<animateTransform> es lo mismo para transformaciones, con un type que indica cuál:

<circle cx="50" cy="50" r="8" fill="currentColor">
  <animateTransform attributeName="transform" type="rotate"
                    from="0 50 50" to="360 50 50"
                    dur="3s" repeatCount="indefinite" />
</circle>

Fíjate en from="0 50 50": la sintaxis de SVG para rotate acepta el ángulo y el centro en la misma función, lo cual resuelve de un plumazo el problema de origen de la lección anterior. Es una de las cosas que SMIL hace más cómodas que CSS.

<animateMotion> mueve un elemento a lo largo de un trazado, y con <mpath> puede referenciar un trazado que ya existe en el documento:

<svg viewBox="0 0 200 100">
  <path id="ruta" d="M 10 80 Q 60 10 110 80 T 190 80"
        fill="none" stroke="currentColor" opacity="0.3" />

  <circle r="6" fill="currentColor">
    <animateMotion dur="4s" repeatCount="indefinite" rotate="auto">
      <mpath href="#ruta" />
    </animateMotion>
  </circle>
</svg>

Ese <mpath href="#ruta"> es una de las cosas más elegantes de SMIL: el trazado que se dibuja y el trazado que se recorre son literalmente el mismo elemento. La alternativa en CSS, offset-path, obliga a repetir la definición del trazado en el CSS, con lo que hay dos copias que mantener sincronizadas.

<set> fija un valor durante un intervalo, sin interpolar. Sirve para atributos que no son interpolables y para cambios discretos.

La temporización es donde SMIL enseña de qué era capaz: begin acepta tiempos, eventos y referencias a otras animaciones.

<circle r="10">
  <animate id="a" attributeName="r" to="20" dur="300ms"
           begin="click" fill="freeze" />
  <animate attributeName="fill-opacity" to="0.3" dur="300ms"
           begin="a.end" fill="freeze" />
</circle>

Un clic dispara la primera animación y el final de la primera dispara la segunda. Sin JavaScript y sin retardos calculados a mano. CSS no tiene nada equivalente.

Los tres casos donde sigue siendo la única opción

Animar atributos que no son propiedades de CSS. Es el caso principal. points, viewBox, x1 e y1 de una línea, el offset de una parada de degradado, patternTransform. Ninguno se puede animar con @keyframes y todos se pueden animar con <animate>.

Temporización declarativa encadenada o por eventos. Secuencias donde cada paso arranca al terminar el anterior, o donde la animación responde a un clic sobre el propio SVG, sin JavaScript.

Un <mpath> que reutiliza un trazado existente. Cuando el trazado que se recorre también se dibuja, mantener una sola definición vale más de lo que parece.

Hay un cuarto caso que se cita a menudo y que hoy es falso: “SMIL es lo único que funciona dentro de un SVG referenciado desde una etiqueta de imagen”. No es cierto. En ese contexto no se ejecuta JavaScript y no se aplican hojas de estilo externas, pero una hoja de estilos incrustada dentro del propio fichero SVG sí funciona, y sus animaciones también. Un SVG autocontenido con su <style> dentro se anima perfectamente desde una etiqueta de imagen.

Las tres razones de fondo para no usar SMIL, y ninguna es el soporte

Como el soporte no es un argumento —está en los tres motores— conviene tener claras las razones reales, porque son de arquitectura y no de compatibilidad. La primera: SMIL no participa en el modelo de tiempo de la plataforma. Sus animaciones no aparecen en getAnimations(), no son objetos Animation, no tienen promesas, no se pueden componer con animation-composition y las herramientas de desarrollo del navegador no las muestran en el panel de animaciones. Todo el instrumental que has aprendido a usar deja de servir, y depurar una secuencia de SMIL se hace leyendo el marcado. La segunda, y es la grave: no hay forma de apagarla con CSS. Una @media (prefers-reduced-motion: reduce) no puede tocar un <animate>, porque no es una declaración de CSS. La única vía es pauseAnimations() sobre el elemento svg desde JavaScript, lo que significa que la accesibilidad de un SVG animado con SMIL depende de que alguien haya escrito ese JavaScript, y en un icono decorativo dentro de un componente reutilizable casi nunca lo ha escrito nadie. Un logotipo que gira sin parar y que no se puede detener con la preferencia del sistema es un problema real para una parte de los usuarios. La tercera: siempre corre en el hilo principal. No hay ruta de compositor para SMIL, así que una animación que en CSS sería gratuita compite con tu JavaScript. La conclusión, entonces, no es la que se repite por ahí sino esta: usa SMIL para lo que solo SMIL puede hacer, y para nada más; y cuando lo uses, añade la pausa desde JavaScript en el mismo commit, porque si no lo haces ahí no lo hará nadie.

La pausa que hay que escribir

Cerrando con lo que la advertencia anterior exige. Si un SVG del proyecto usa SMIL, esto tiene que existir en algún sitio:

const reducir = matchMedia('(prefers-reduced-motion: reduce)');

function aplicarPreferencia() {
  document.querySelectorAll('svg').forEach((svg) => {
    if (typeof svg.pauseAnimations !== 'function') return;
    if (reducir.matches) svg.pauseAnimations();
    else svg.unpauseAnimations();
  });
}

aplicarPreferencia();
reducir.addEventListener('change', aplicarPreferencia);

pauseAnimations() y unpauseAnimations() son métodos del elemento svg y detienen el reloj de SMIL de ese documento, dejando las animaciones congeladas en su estado actual. La comprobación de tipo antes de llamar cubre el caso de que el elemento no sea el raíz de un documento SVG con reloj propio.

Que este código haga falta —y que sea JavaScript, y que haya que acordarse— es en sí mismo el argumento más sólido para preferir CSS siempre que la vía CSS exista. En CSS, la misma preferencia se respeta con tres líneas dentro de una media query, sin script, sin registro de listeners y sin que nadie tenga que acordarse de nada.