Animación (Motion)
Esta habilidad construye animaciones. Extiende la sección de animación de product-ui/references/values.md, que contiene los números acordados (curvas de easing, escala al presionar, rebote, escalonamiento, tabla de duraciones, duraciones de toast y diseño, distancias de viaje, relación salida-entrada). Esos números no se repiten aquí; esta habilidad añade el orden de las decisiones, los tokens, las recetas y los modos de fallo.
Antes de cualquier decisión, aplica product-ui §1 (el diario de gusto y precedencia) y §5 (qué habilidad es dueña de qué).
El orden de las decisiones
Recorre esto de arriba a abajo. Cada paso puede finalizar el trabajo. Cero líneas de animación es un resultado válido, y así lo indicas.
1. ¿Debería moverse? El filtro de frecuencia
Pregunta cuántas veces al día un usuario ve esto. Los niveles están en values.md. En la práctica:
- Iniciado por teclado, o visto 100+ veces al día: sin movimiento. Por qué: el movimiento repetido se convierte en retraso.
- Visto decenas de veces al día (hover, cambio de pestaña, selección de fila): ninguno, o un cambio de color instantáneo.
- Los datos que el usuario está leyendo o sobre los que actúa (un número en una tabla, un gráfico en un panel en vivo) nunca se mueven por estilo. Por qué: el movimiento desvía la atención de lo que se está leyendo.
- Si el filtro dice no, escribe ninguna animación. Indica qué nivel alcanzó, y ofrece la alternativa estática (un cambio de estado instantáneo).
2. Nombra el propósito
Elige una palabra: espacial (de dónde vino o a dónde fue), estado (esto cambió), retroalimentación (te escuché), continuidad (evita un salto), explicación (solo momentos raros). Si no puedes nombrarla, detente. Por qué: "se ve bonito" en un elemento frecuente es la principal fuente de interfaz que se siente lenta.
3. Elige la herramienta: la más barata que funcione
| Necesidad | Herramienta |
|---|---|
| Un estado alternado por clase, atributo, hover o presionar | CSS transition |
Entrada desde display: none, popover o diálogo | @starting-style + transition-behavior: allow-discrete |
| Una secuencia única, escalonamiento, o animación que debe ejecutarse mientras la página está ocupada | CSS @keyframes |
JS decide cuándo; necesitas cancelar, revertir, finished, o FLIP | WAAPI el.animate() |
| Springs, transferencia de velocidad, salida de un nodo React que se desmonta, diseño compartido | Motion (motion/react) |
Si la cosa es un componente completo (menú, toast, cajón), usa una librería que maneje el foco, y pregunta pick-library. Nunca instales Motion para un fade. Por qué: CSS se ejecuta fuera del hilo principal y no cuesta nada en el bundle.
4. Propiedades
- Anima
transformyopacity;filtersolo para un desenfoque de intercambio ≤ 2px. Nombra cada propiedad; nuncatransition: all. - Cambios de altura y diseño: consulta
references/recipes.md(filas de grid, FLIP). Nunca animeswidth,top,leftomarginpara mover algo. Por qué: cada una fuerza el diseño en cada fotograma. - Nunca animes
box-shadow. Desvanece la opacidad de un pseudo-elemento en su lugar. - Los porcentajes de
translateson relativos al tamaño propio del elemento, así quetranslateY(100%)ajusta cajones y toasts de cualquier altura.
5. Curva y duración, emparejadas por uso
Nunca elijas el número más cercano a tu suposición. Indica el uso ("cierre de menú", "cambio de icono"), luego toma esa fila de references/tokens.md. Si ninguna fila coincide con el uso, añade un nuevo token con el nombre del uso; no adaptes uno existente. Por qué: un token es una declaración sobre para qué es la animación, así que un número cercano con el uso incorrecto es el token equivocado.
- Entrada o salida:
--ease-out. Moviéndose en pantalla:--ease-in-out. Cajones:--ease-drawer. Color:ease. Spinners:linear. Nuncaease-in. - Cualquier cosa arrastrable o interrumpible en pleno vuelo usa un spring, no una curva. Por qué: una curva no puede aceptar la velocidad que tenía la mano del usuario.
- Sensación lenta: recorta la duración primero, luego la distancia, y solo entonces toca la curva.
6. Interrupción
- Cualquier cosa que el usuario pueda activar dos veces en un segundo es una transición o un spring, nunca
@keyframes. Por qué: una transición reorienta desde el valor actual; los keyframes se reinician y saltan. - WAAPI: cancela la animación anterior después de leer el valor actual, y comienza la nueva desde ese valor (
references/recipes.md). - Una alternancia pulsada 10 veces debe terminar en el estado correcto sin saltos. Pruébalo.
7. Salida
- La salida por defecto es un fade, más corto que la entrada (el archivo de tokens tiene cada par).
- Cualquier cosa con una dirección (cajón, panel, toast deslizado) se va por donde vino.
- El elemento que sale deja de ocupar espacio de inmediato, así que los hermanos cierran el hueco mientras se desvanece.
- Las salidas son simultáneas y nunca escalonadas. Por qué: el usuario ya decidió irse.
8. Movimiento reducido, enviado con la animación
Opacidad y color se quedan. Los deslizamientos, escalas, desenfoques y rebotes se van; los deslizamientos se convierten en fades, el rebote se convierte en 0. Nunca uses el movimiento como la única señal. Patrón en references/recipes.md.
Coreografía
Asigna un rol a cada elemento en movimiento antes de temporizarlo:
| Rol | Tratamiento |
|---|---|
| Principal (la única cosa a notar) | Se mueve primero, viaja más |
| Secundario | Sigue, viaja menos |
| Decoración | Se desvanece en su lugar, o nada |
| Estático (contenedores, chrome) | Nunca se mueve |
- Una entrada por contenedor. Si el panel se desliza hacia adentro, su contenido ya está ahí. Por qué: dos entradas superpuestas hacen que el usuario espere el doble.
- Ordena por importancia, no por orden del DOM. El retraso se mantiene en los pasos de values.md (40ms entre elementos, 100ms entre título, texto y acciones). Varía la distancia y la duración por rol en lugar del retraso.
- Limita el escalonamiento de una lista al índice 6 (un retraso máximo de 240ms); los elementos posteriores comparten ese retraso. La entrada funciona mientras se reproduce.
- Nunca escaleones un menú, paleta de comandos, contenido de pestaña o cualquier apertura repetida.
- Quédate quieto: cualquier cosa que el usuario esté a punto de hacer clic, cualquier cosa ya en pantalla cuando la página se carga de nuevo, y una actualización que llegue dentro de 150ms de la anterior (salta al estado final).
- La dirección sigue el significado: lo siguiente entra desde la derecha, lo anterior desde la izquierda; las cosas que el usuario activó crecen hacia afuera desde el activador.
Cargar cuando sea necesario
- references/tokens.md: carga para cualquier valor exacto de duración, distancia, escala, desenfoque, easing o spring, y para tokenizar una base de código existente.
- references/recipes.md: carga al construir un menú, popover, diálogo, cajón, toast, entrada de lista, cambio de icono, cambio de altura, indicador de pestaña o cambio de número.
- references/gestures.md: carga para arrastrar, deslizar, deslizar rápido, paneles, o cualquier cosa que siga a un puntero.
- references/performance.md: carga cuando la animación pierde fotogramas, antes de recurrir a
will-change, y cuando las actualizaciones llegan rápido.
Pares
| Roto | Arreglado | Por qué |
|---|---|---|
.menu { transition: all 300ms ease-in } | transition: opacity 200ms, transform 200ms var(--ease-out) | Propiedades nombradas; ease-in es lento; menús ≤ 200ms |
scale(0) a scale(1) | scale(0.95) y opacity: 0 a 1 | Nada aparece de la nada |
| Popover se escala desde su centro | transform-origin: var(--transform-origin) | Debería crecer desde su activador |
.open { animation: slide-in 300ms } | transition: transform en el estado alternado | Los keyframes se rompen al ser interrumpidos |
Cada fila animation-delay: i * 120ms | min(i, 6) * 40ms | La última fila llega en menos de 300ms |
| El cajón se desvanece después de deslizarse hacia adentro | Se desliza hacia atrás por donde vino | La dirección es la explicación |
Liberación de arrastre en un ease-out fijo | Un spring alimentado con la velocidad de liberación | El impulso de la mano continúa |
setY(y) en pointermove | el.style.transform = ... | Sin re-render por fotograma |
Antes de terminar
Busca en el diff, luego verifica.
| Busca | Arregla |
|---|---|
transition: all, will-change en *, ease-in | Propiedades nombradas, elimina, --ease-out |
scale(0), transform-origin: center en un popover | 0.95, el origen del activador (los diálogos se quedan centrados) |
@keyframes en una clase que se alterna | Una transición |
| Una duración superior a su fila en tokens.md, o un número no en tokens.md | El token del uso |
Animación en manejadores keydown o una paleta | Elimínala |
width, height, top, left, box-shadow en una transición | transform, opacity, o la receta |
Sin ruta prefers-reduced-motion | Añádela |
Verifica en la aplicación en ejecución: reprodúcela a 4× (panel Animations de DevTools, o multiplica las duraciones), pulsa cualquier alternancia repetidamente, prueba con movimiento reducido activado, y acelera la CPU 4×. Indica cuál de estos no pudiste ejecutar.