Animación
Cuándo mover algo, cuánto debe durar y con qué curva. Con lo que dicen Apple, Google, IBM y Emil Kowalski, y dónde se contradicen.
Cómo leer esta guía
Tres tipos de párrafo, siempre marcados:
- Citado — lo dice una fuente y el texto original está en
sources/. Lleva su referencia. - Verificado — un número sacado del código o de la documentación del emisor, no de memoria.
- Decisión Sensa — nuestra postura. No la respalda nadie más; es discutible y por eso va marcada.
Si algo no lleva marca, es que falta trabajo. No lo trates como establecido.
La decisión rápida
Antes de elegir duración o curva, dos preguntas que descartan la mayor parte del trabajo:
- ¿El usuario va a ver esto más de cien veces al día? Entonces no lo animes. (Citado — Emil Kowalski señala Raycast como ejemplo de interfaz que no anima precisamente porque se abre cientos de veces al día. Apple lo dice con otras palabras: «in apps, generally avoid adding motion to UI interactions that occur frequently».)
- ¿La acción salió del teclado? No la animes. (Citado — Emil Kowalski: «never animate keyboard initiated actions».)
Si pasa el filtro:
| Qué ocurre | Curva |
|---|---|
| Algo entra o sale de pantalla | ease-out (entrada) / ease-in (salida) |
| Algo que ya está en pantalla se mueve o cambia de tamaño | ease-in-out |
| Hover, color, estado sutil | ease |
| Movimiento continuo (marquesina, progreso temporal) | linear |
(Citado — es la división de Material: la curva de deceleración «elements enter the screen at full velocity and slowly decelerate», la de aceleración «elements leave the screen at full velocity», y la estándar para lo que permanece en pantalla. Carbon usa la misma tripleta con los nombres entrance / exit / standard.)
Duración: aquí las fuentes no se ponen de acuerdo
Esto es lo más útil de la guía, porque quien lea sólo una fuente se llevará una idea equivocada de que hay consenso.
| Fuente | Qué prescribe |
|---|---|
| Emil Kowalski | Por debajo de 300 ms. (Citado) |
| Rauno Freiberg | «Animation duration should not be more than 200ms for interactions to feel immediate». (Citado) |
| Material 1 | 300 ms típico en móvil; entrada 225 ms; salida 195 ms; pantalla completa hasta 375 ms; «transitions that exceed 400ms may feel too slow». (Verificado) |
| Material 3 | Escala de tokens, sin prescribir uno: short 50/100/150/200, medium 250/300/350/400, long 450/500/550/600, extra-long 700/800/900/1000 ms. (Verificado en el .scss del design system) |
| Carbon (IBM) | Escala por función: 70 ms micro-interacción, 110 ms fundido, 150 ms expansión corta, 240 ms toast, 400 ms expansión grande, 700 ms atenuado de fondo. (Verificado) |
| Apple | Ningún número. Sólo «aim for brevity and precision». (Citado) |
La contradicción es aparente. Rauno y Emil hablan de respuesta a un gesto del usuario, donde cualquier retraso se lee como lentitud. Material y Carbon cubren además transiciones de superficie — una hoja que ocupa la pantalla, un fondo que se atenúa — donde 200 ms resulta brusco. Carbon lo explicita: la duración «should be dynamic based on the size of the animation; the larger the change in distance (traveled) or size (scaling) of the element, the longer the animation takes». Material 1 dice lo mismo y añade dos escalas que nadie más documenta: tablet un 30 % más lento que móvil, wearable un 30 % más rápido. (Verificado)
Decisión Sensa. Dos reglas, no una:
- Lo que responde a un gesto directo: 100–200 ms.
- Lo que cambia una superficie entera: 200–400 ms, subiendo con el tamaño.
- Nunca por encima de 400 ms en producto. En marketing sí, es otro juego.
- La salida un 20 % más rápida que la entrada. (Se apoya en Material 1: entrada 225 ms, salida 195 ms — que es justo un 13 %. Redondeamos a 20 %; el principio está citado, el número es nuestro.)
Curvas verificadas
Todas sacadas del código o la documentación de cada emisor, no de memoria.
Material 3 — md.sys.motion (Verificado)
--md-easing-emphasized: cubic-bezier(0.2, 0, 0, 1);
--md-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
--md-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
--md-easing-standard: cubic-bezier(0.2, 0, 0, 1);
--md-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1);
--md-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1);
--md-easing-legacy: cubic-bezier(0.4, 0, 0.2, 1);
Nota: emphasized y standard tienen hoy el mismo valor en el archivo de tokens. No es un error de transcripción, es lo que hay en el fuente.
Material 1 — las curvas clásicas (Verificado)
/* standard */ cubic-bezier(0.4, 0.0, 0.2, 1);
/* deceleration */ cubic-bezier(0.0, 0.0, 0.2, 1);
/* acceleration */ cubic-bezier(0.4, 0.0, 1, 1);
/* sharp */ cubic-bezier(0.4, 0.0, 0.6, 1);
La sharp es la menos conocida y la más útil: es para lo que sale de pantalla pero puede volver en cualquier momento.
Carbon — productive vs expressive (Verificado)
| Productive | Expressive | |
|---|---|---|
| Standard | cubic-bezier(0.2, 0, 0.38, 0.9) |
cubic-bezier(0.4, 0.14, 0.3, 1) |
| Entrance | cubic-bezier(0, 0, 0.38, 0.9) |
cubic-bezier(0, 0, 0.3, 1) |
| Exit | cubic-bezier(0.2, 0, 1, 0.9) |
cubic-bezier(0.4, 0.14, 1, 1) |
La distinción de Carbon es la más transferible a nuestro trabajo: productive para lo que el usuario hace todo el día, expressive para los momentos que importan. Es la misma idea que «no animes lo que se ve cien veces», pero graduada en lugar de binaria.
tw-animate-css — lo que shadcn trae de fábrica (Verificado)
animate-in y animate-out usan 150 ms y ease por defecto. Si usas shadcn sin tocar nada, esos son tus valores reales. Están dentro del rango razonable, así que la mayor parte del tiempo lo correcto es no tocarlos.
Rendimiento
(Citado — Emil Kowalski) Animar sólo transform y opacity: son las que no disparan layout ni paint. Evitar padding, margin, width, height. Objetivo 60 fps. Las animaciones CSS y la Web Animations API corren fuera del hilo principal, lo que importa cuando ese hilo está ocupado.
(Citado — Rauno Freiberg) Las animaciones en bucle deben pausarse cuando no están visibles. Cambiar de tema no debe disparar transiciones.
(Citado — Material 1) En web, las transiciones complejas sueltan frames si no están aceleradas por GPU; una duración más corta lo disimula porque termina antes.
Accesibilidad
(Citado — WCAG 2.3.3, nivel AAA) «Motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed». Esencial significa que quitarlo cambiaría la información o la función y no hay otra manera de lograrlo. Ojo con el criterio hermano 2.2.2, que cubre lo que se mueve solo sin que el usuario lo dispare.
(Citado — Apple) «Make motion optional. Not everyone can or wants to experience the motion in your app or game, so it's essential to avoid using it as the only way to communicate important information». Apple recomienda apoyar el feedback visual con hápticos y sonido.
(Citado — Apple) «Let people cancel motion. As much as possible, don't make people wait for an animation to complete before they can do anything, especially if they have to experience the animation more than once.» Esto se olvida siempre y es lo que más molesta.
Decisión Sensa. Cada elemento animado lleva su propia media query. Sin excepciones para opacidad o color.
@media (prefers-reduced-motion: reduce) {
.thing { animation: none; transition: none; }
}
Anti-patrones
- Escalar un diálogo desde
0. Empieza en0.95–0.8y acompaña con opacidad. (Citado — Rauno: «avoid scaling dialogs from 0 to 1») - Hundir un botón a
0.8al pulsarlo.0.96–0.9basta. (Citado — Rauno) - Hover visible al tocar en móvil. Envolver en
@media (hover: hover). (Citado — Rauno) - Animar lo que el usuario repite todo el día. (Citado — Apple y Emil, por separado)
- Que dos elementos que se mueven juntos tengan curvas o duraciones distintas. Modal y fondo son una unidad.
- Movimiento en la periferia visual en visionOS: molesta físicamente. (Citado — Apple)
Checklist de revisión
- ¿Esta animación tiene una función, o está porque quedaba bien?
- ¿Cuántas veces al día la verá la misma persona?
- ¿Es un gesto directo (100–200 ms) o una superficie (200–400 ms)?
- ¿Entra, sale o se mueve dentro de pantalla? ¿La curva corresponde?
- ¿Sólo
transformyopacity? - ¿Se puede interrumpir a media animación?
- ¿Tiene
prefers-reduced-motion? - ¿Los elementos emparejados comparten curva y duración?
- ¿Se puede cancelar, o hay que esperar a que termine?
Lo que falta
- Springs: Emil los recomienda para arrastre y gestos, y Compose los documenta, pero no hemos verificado parámetros concretos de ninguna fuente. Sin cifras hasta entonces.
- Apple no publica duraciones ni curvas en el HIG. Si necesitamos números de Apple hay que ir a la API de SwiftUI, no a la guía.
- shadcn no tiene guía de motion; lo único citable es el código de tw-animate-css.