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

Transformaciones y sistemas de coordenadas

Por qué rotar una forma SVG la manda al otro lado del lienzo, qué hace transform-box, y la diferencia entre el atributo transform y la propiedad.

⏱ 18 min

Todo el que anima SVG por primera vez tropieza con lo mismo: aplica una rotación a un rectángulo y el rectángulo describe una órbita enorme alrededor de una esquina del lienzo en lugar de girar sobre sí mismo. No es un bug ni una rareza histórica: es la consecuencia exacta de dos valores iniciales bien documentados que difieren de los de HTML. Entenderlos convierte el problema en una línea de CSS.

🎯 Al terminar esta lección sabrás
  • Explicar por qué el origen de transformación por defecto en SVG no es el centro del elemento.
  • Corregir el problema con transform-box y transform-origin.
  • Distinguir el atributo transform de la propiedad de CSS del mismo nombre.
  • Anidar transformaciones con grupos sin pelearse con las coordenadas.

Los dos valores iniciales que lo explican todo

Hay dos hechos, los dos verificables en la especificación de transformaciones de CSS, y su combinación produce el efecto desconcertante.

Primero: el valor inicial de transform-box es view-box. La caja de referencia de un elemento SVG es, por defecto, el viewBox del viewport SVG más cercano, no la caja del propio elemento. Es decir: cuando el motor tiene que resolver un porcentaje o un origen, lo hace contra el lienzo entero.

Segundo: para los elementos SVG sin caja de layout de CSS asociada, el valor usado inicial de transform-origin es 0 0. No 50% 50%, que es lo que aplica en HTML. Cero cero. Y ese cero cero es la esquina superior izquierda de la caja de referencia, que por el punto anterior es el lienzo.

Juntando los dos: una rotación sobre una forma SVG gira alrededor del origen del viewBox. Si el rectángulo está a doscientas unidades de esa esquina, describe un arco de radio doscientos. Es exactamente lo que uno observa y no tiene nada de misterioso una vez enunciados los dos valores.

La corrección es una línea y media:

.pieza {
  transform-box: fill-box;
  transform-origin: center;
  animation: girar 2s linear infinite;
}

@keyframes girar {
  to { rotate: 1turn; }
}

transform-box: fill-box cambia la caja de referencia a la caja delimitadora del propio objeto, y a partir de ahí transform-origin: center significa lo que uno espera: el centro de la forma. fill-box está en Chromium desde la 64, Firefox desde la 55 y Safari desde la 11, así que es completamente seguro.

Los cinco valores de transform-box

Merece la pena conocerlos porque no todos tienen el mismo soporte y porque dos de ellos resuelven casos concretos.

Valor Caja de referencia Soporte
view-box El viewBox del viewport SVG más cercano Universal, es el inicial
fill-box La caja delimitadora del objeto, sin contar el trazo Universal
stroke-box La caja delimitadora incluyendo el trazo Chromium 118, Firefox 125, Safari 13.1
content-box La caja de contenido, para elementos con layout de CSS Chromium 118, Firefox 125, Safari 13.1
border-box La caja de borde, para elementos con layout de CSS Chromium 118, Firefox 55, Safari 11

El que resuelve un caso real y poco conocido es stroke-box. Con fill-box, la caja no incluye el grosor del trazo, así que el centro de una forma con un trazo muy grueso y asimétrico no coincide con su centro visual. Para un icono de línea con trazo de cuatro unidades sobre una forma pequeña, la diferencia se ve. Ahora bien, stroke-box llegó a Chromium en la 118 y a Firefox en la 125, así que si te importan versiones anteriores, fill-box es la elección segura.

El atributo y la propiedad

SVG tiene un atributo transform desde antes de que CSS tuviera la propiedad, y hoy conviven. No son lo mismo y las diferencias importan.

Sintaxis. El atributo usa la sintaxis de SVG: transform="translate(10 20) rotate(45)", con espacios o comas como separador y sin unidades, porque los números son unidades de usuario. La propiedad de CSS usa la sintaxis de CSS: transform: translate(10px, 20px) rotate(45deg), con comas y con unidades obligatorias donde CSS las exige.

Precedencia. El atributo transform es un atributo de presentación, así que se comporta como una declaración de autor con especificidad cero. Cualquier regla de CSS que declare transform lo pisa por completo. No se combinan: la propiedad sustituye al atributo, no se suma a él.

Eso último es el origen de un fallo clásico. Un SVG exportado de una herramienta de diseño trae transform="translate(24 16)" en un grupo para colocarlo en su sitio. Añades una animación de CSS con transform: rotate(...) sobre ese mismo grupo y el elemento salta a la esquina, porque tu propiedad ha sustituido la traslación de colocación. La solución no es replicar la traslación en el CSS —eso acopla la animación a la posición— sino separar las responsabilidades en dos elementos: un grupo exterior que coloca con el atributo y uno interior que anima con CSS.

<g transform="translate(24 16)">
  <g class="pieza">
    <path d="..." />
  </g>
</g>
.pieza {
  transform-box: fill-box;
  transform-origin: center;
  animation: girar 2s linear infinite;
}

Esa separación es la práctica correcta en cualquier SVG que vayas a animar, y conviene aplicarla desde el principio aunque hoy no haya conflicto: el día que lo haya, el arreglo es gratis.

Las propiedades individuales evitan el problema entero, y son la razón para dejar de escribir transform

Hay una herramienta que mucha gente no ha incorporado todavía y que conviene adoptar sin reservas: las propiedades individuales translate, rotate y scale. No son abreviaturas de transform: son propiedades independientes, cada una con su propio valor calculado y su propia animación, y se componen en un orden fijo definido por la especificación —traslación, rotación, escalado y por último lo que hubiera en transform—, de modo que ya no hay que recordar en qué orden va cada función dentro de una lista. Las dos ventajas para animar SVG son grandes. La primera: dos animaciones distintas pueden tocar piezas distintas sin pisarse. Con transform, dos animaciones sobre el mismo elemento compiten por una única propiedad y gana una; con las individuales, una puede animar scale y otra rotate a la vez, que es exactamente lo que quieres para un icono que late mientras gira, y ahorra tener que fusionar los dos efectos en un solo @keyframes. La segunda: el orden fijo elimina una clase entera de bugs, los que vienen de que rotate antes o después de translate dentro de una lista de funciones da resultados distintos. Ahora el matiz que hay que tener presente y que anula el atajo tentador: las individuales no te libran de separar la colocación de la animación. Como se componen por fuera de transform, animar rotate sobre un grupo que lleva transform="translate(24 16)" en el marcado sigue produciendo una órbita, porque el origen de la rotación se resuelve sobre la caja del elemento sin su propia traslación aplicada. La estructura de dos grupos —uno que coloca, otro que anima— sigue siendo la respuesta correcta; lo que aportan las propiedades individuales es que dentro del grupo que anima puedas repartir el trabajo en varias animaciones independientes. La recomendación práctica queda así: coloca con el atributo en un grupo exterior, y anima con translate, rotate y scale en un grupo interior con transform-box: fill-box.

Anidar sin sufrir

La última pieza es entender que cada elemento svg establece un sistema de coordenadas nuevo mediante su viewBox, y que los grupos heredan el de su padre transformado. De ahí dos consejos que ahorran mucho tiempo.

Usa grupos como capas de responsabilidad. Un grupo para colocar, otro para animar, otro para escalar. Cada uno con una sola preocupación. Es más marcado, y a cambio cada transformación se puede tocar sin mirar las demás.

Piensa en unidades de usuario, no en píxeles. Dentro del SVG, translate(10 0) mueve diez unidades del viewBox, que en pantalla serán los píxeles que toque según el tamaño renderizado. Eso hace que las animaciones sean automáticamente responsivas: la misma animación se ve proporcionada en un icono de dieciséis píxeles y en una ilustración de mil. Es una propiedad excelente y se pierde en cuanto alguien escribe translate(10px, 0) con unidades de CSS, porque entonces son diez píxeles de verdad y la animación deja de escalar con el gráfico.