wandres.dev
TRANSFORMACIONES · El pipeline 2D y 3D

translate, rotate y scale frente a transform

Por qué la especificación partió transform en tres propiedades independientes, qué gana la cascada con ello, y el orden fijo en que se aplican.

⏱ 18 min

Durante quince años, mover, girar y escalar un elemento se hacía con una sola propiedad: transform. Eso significaba que dos reglas que querían cosas distintas —una centrar con translate(-50%, -50%), otra girar al pasar el ratón— no podían coexistir, porque la cascada resuelve declaraciones enteras y la última se llevaba las dos. CSS Transforms Level 2 partió la propiedad en tres, y el cambio no es cosmético: convierte un valor monolítico en tres canales independientes que cascadean, heredan reglas de interpolación y se animan por separado.

🎯 Al terminar esta lección sabrás
  • Escribir la misma transformación con transform y con las propiedades individuales.
  • Explicar por qué transform no se puede componer entre reglas y las individuales sí.
  • Recitar el orden fijo en que el motor aplica translate, rotate, scale y transform.
  • Decidir cuál de las dos formas usar según el caso.

El problema que resuelve

transform acepta una lista de funciones: transform: translateX(10px) rotate(15deg) scale(1.2). La lista es el valor. La cascada no sabe que dentro hay tres cosas conceptualmente distintas; ve una declaración y aplica la ganadora entera.

/* patrón clásico de centrado */
.tarjeta { position: absolute; inset-block-start: 50%; transform: translateY(-50%); }

/* y ahora quieres que crezca al pasar el ratón */
.tarjeta:hover { transform: scale(1.05); }   /* rompe el centrado */

El :hover gana por especificidad y su valor sustituye al anterior. El elemento deja de estar centrado en el momento exacto en que pasas el ratón. La solución tradicional era repetir: transform: translateY(-50%) scale(1.05). Funciona, pero acopla las dos reglas: si mañana cambias el centrado, tienes que acordarte de tocar el hover.

Con las propiedades individuales el acoplamiento desaparece porque cada aspecto vive en su propia propiedad y cada una compite en la cascada por separado.

.tarjeta { position: absolute; inset-block-start: 50%; translate: 0 -50%; }
.tarjeta:hover { scale: 1.05; }

Ahora :hover solo declara scale. La declaración de translate sigue viva porque nadie la ha sobrescrito. Es exactamente el mismo razonamiento por el que margin-block-start y margin-inline-start existen además de margin: la granularidad de la propiedad es la granularidad de la cascada.

Las tres propiedades están disponibles en los cuatro motores desde agosto de 2022, cuando Chrome 104 fue el último en implementarlas; Firefox las tenía desde la 72 y Safari desde la 14.1. Son Baseline ampliamente disponible.

La sintaxis de las tres

translate acepta de uno a tres valores: eje x, eje y, eje z. Los dos primeros admiten porcentajes, que se resuelven contra la caja de borde del propio elemento —igual que en translate()—; el tercero solo admite longitudes absolutas, porque no hay ninguna dimensión contra la que resolver un porcentaje en profundidad.

.a { translate: 20px; }            /* solo x */
.b { translate: 20px 1rem; }       /* x e y */
.c { translate: 0 0 -40px; }       /* aleja en z */
.d { translate: -50% -50%; }       /* centrado clásico */

rotate acepta un ángulo suelto, que gira en el plano de la pantalla, o un eje seguido de un ángulo. El eje puede escribirse con las palabras clave x, y, z o como un vector de tres números.

.e { rotate: 45deg; }              /* equivale a rotate: z 45deg */
.f { rotate: x 45deg; }            /* vuelca hacia atrás */
.g { rotate: 1 1 0 45deg; }        /* eje arbitrario, vector normalizado por el motor */

scale acepta de uno a tres números o porcentajes. Un solo valor escala x e y por igual, pero no toca z: scale: 2 equivale a scale: 2 2 1, no a scale3d(2,2,2). Es la diferencia con la función scale(), que también deja z a 1, y con scale3d(), que sí lo cambia.

.h { scale: 1.5; }
.i { scale: 2 0.5; }
.j { scale: 150%; }                /* los porcentajes son válidos aquí */

Ninguna de las tres puede expresar skew(), matrix(), matrix3d() ni perspective(). Para eso sigue haciendo falta transform, y las dos formas conviven sin problema en el mismo elemento.

El orden de aplicación es fijo

Este es el punto que hay que memorizar: el orden en que declaras las propiedades individuales no importa. La especificación define un orden de aplicación fijo, y el motor lo respeta siempre.

flowchart LR
A[Caja sin transformar] --> B[translate]
B --> C[rotate]
C --> D[scale]
D --> E[transform]
E --> F[Caja final en pantalla]
style A fill:#89b4fa,color:#11111b
style B fill:#cba6f7,color:#11111b
style C fill:#cba6f7,color:#11111b
style D fill:#cba6f7,color:#11111b
style E fill:#f9e2af,color:#11111b
style F fill:#a6e3a1,color:#11111b

Primero se traslada, después se rota, después se escala, y por último se aplica la lista de transform si la hay. Escribir scale: 2; translate: 10px; produce exactamente el mismo resultado que translate: 10px; scale: 2;, y ambos equivalen a transform: translate(10px) scale(2).

La consecuencia práctica más útil es que transform siempre va al final. Si necesitas una función que las individuales no tienen, sabes exactamente dónde se va a insertar en la cadena:

.tarjeta {
  translate: 10px 0;
  rotate: 5deg;
  transform: skewX(-8deg);   /* se aplica DESPUES de trasladar y rotar */
}
/* idéntico a: transform: translate(10px, 0) rotate(5deg) skewX(-8deg); */

La consecuencia incómoda es la contraria: si necesitas un orden distinto —escalar antes de trasladar, por ejemplo— las individuales no te sirven y tienes que volver a la lista de transform, donde el orden lo pones tú. Esto importa más de lo que parece, y es el tema de la siguiente lección.

No son un shorthand de transform, y eso tiene consecuencias

Aquí está el matiz que rompe scripts en producción. translate, rotate y scale no son subpropiedades de transform: son cuatro propiedades hermanas e independientes que el motor compone al pintar. De ahí salen tres consecuencias que nadie te avisa. Primera: transform: none no cancela un translate declarado en otra regla; para borrarlo todo necesitas las cuatro a none, o bien all: unset. Segunda: getComputedStyle(el).transform devuelve únicamente el valor de la propiedad transform; si el elemento está desplazado por translate, ese desplazamiento no aparece en la cadena que lees, y cualquier código que reconstruya la posición a partir de la matriz computada te dará un resultado incompleto. Tercera: la herencia de por qué transform nació como una lista es histórica —viene del atributo transform de SVG, donde también es una lista de funciones— y el precio de esa herencia es exactamente el problema de cascada de esta lección. Cuando integres una librería que escribe transform por JavaScript, decide desde el principio quién es el dueño de qué canal: si la librería escribe transform y tú escribes translate, no chocáis, pero tampoco os veis.

⚔️ Comprueba el orden y la independencia
  1. Escribe scale: 2; translate: 50px; y translate: 50px; scale: 2; en dos elementos hermanos y mide con el inspector si acaban en el mismo sitio.
  2. Compara ese resultado con transform: scale(2) translate(50px) y explica la diferencia de desplazamiento observada.
  3. Centra un elemento con translate: -50% -50% y añade .el:hover { scale: 1.1 }. Verifica que el centrado sobrevive.
  4. Anima con @keyframes un transform: rotate(0deg) a transform: translateX(100px) rotate(360deg) y observa el resultado. Repite con rotate y translate individuales.
  5. Combina rotate: y 40deg con transform: skewX(10deg) y deduce del resultado en qué orden se compusieron.