Sistema de movimiento
Motion Spec
Todo lo que se mueve en las páginas del sitio, con su curva, su duración y su disparador. Pensado para entregarse a quien programe: cada fila se traduce directo a una variante de Framer Motion.
- Versión
- 1.0
- Librería
- framer-motion 11
- FPS objetivo
- 60
- Estado
- Borrador
Principios
-
01
El movimiento explica, no decora.
Cada animación señala jerarquía o relación entre elementos. Si se puede quitar sin perder información, se quita.
-
02
Nada supera los 700 ms.
Por encima de eso el usuario espera en lugar de percibir. Las entradas de sección son lo más lento del sistema.
-
03
Una dirección por sección.
Todo entra de abajo hacia arriba. Sin movimientos cruzados ni elementos que lleguen desde lados opuestos.
-
04
Se anima una vez.
Las entradas usan viewport once:true. Volver a subir no vuelve a disparar nada: es molesto y rompe la lectura.
Tokens
Seis tokens. Nada fuera de esta lista.
Definir cada curva como constante evita que el sitio acumule veinte easings distintos.
| Token | Curva | Duración | Uso |
|---|---|---|---|
| enter | cubic-bezier(0.16, 1, 0.3, 1) | 700 ms | Entrada de secciones y bloques grandes al aparecer en pantalla |
| text | cubic-bezier(0.25, 1, 0.5, 1) | 500 ms | Titulares línea por línea, párrafos y filas de lista |
| page | cubic-bezier(0.65, 0, 0.35, 1) | 400 ms | Transición entre páginas: salida 200 ms, entrada 400 ms |
| hover | spring · stiffness 220 · damping 30 | ≈ 350 ms | Tarjetas, botones y flechas bajo el puntero |
| marquee | linear · bucle infinito | 30 s | Franja horizontal con las trece compañías |
| parallax | linear · ligado al scroll | — | Imágenes a sangre, factor de desplazamiento 0.12 |
Inventario
Cada elemento que se mueve, y por qué.
«i» es el índice del elemento dentro de su grupo. Un delay de 70 · i significa 0, 70, 140, 210 ms…
| Sección | Elemento | Movimiento | Disparador | Delay |
|---|---|---|---|---|
| 01 — Home | ||||
| Nav | Barra completa | Fondo a rgba(8,9,10,.72) con blur de 12 px; altura 92 → 68 px | scroll > 40px | — |
| Hero | Eyebrow | Regla scaleX 0 → 1 desde la izquierda; texto opacity 0 → 1 | montar | 0 |
| Hero | Titular (3 líneas) | Máscara por línea: y 100% → 0 dentro de un contenedor con overflow oculto | montar | 80 · i |
| Hero | Tabla de datos | Filas y 12 → 0 con opacity, escalonadas | montar | 300 + 60 · i |
| Hero | Sub y botones | y 16 → 0 con opacity | montar | 320 / 380 |
| Hero | Imagen a sangre | scale 1.08 → 1 en 900 ms, después parallax y 0 → −60 px | montar + scroll | 200 |
| Franja | 9 nombres | Marquee horizontal infinito; se detiene al pasar el puntero | siempre | — |
| Manifiesto | Declaración | Máscara por línea | 40% en viewport | 90 · i |
| Manifiesto | 3 principios | y 20 → 0 con opacity | viewport | 90 · i |
| Portafolio | 2 destacadas | Imagen scale 1.05 → 1; en hover scale 1.02 y flecha rota 45° | viewport / hover | 120 · i |
| Portafolio | 7 filas | y 16 → 0; en hover fondo #101114, flecha x + 6 px, nombre a marfil | viewport / hover | 55 · i |
| Verticales | 5 columnas | opacity 0 → 1 con y 18 → 0 | viewport | 70 · i |
| Modelo | Fondo de página | Crossfade tinta → marfil en 600 ms al cruzar el borde | scroll | — |
| Modelo | 4 cifras | Conteo numérico de 0 al valor en 1.2 s con easing text | viewport | 100 · i |
| CTA | Halo ámbar | opacity 0 → 1 con scale 0.9 → 1 en 900 ms | viewport | 0 |
| CTA | Titular y botones | Máscara por línea; botones y 12 → 0 | viewport | 100 / 300 |
| Footer | Wordmark gigante | y 40 → 0 con opacity, easing enter | viewport | 0 |
| 02 — Portafolio | ||||
| Cabecera | Titular | Máscara por línea | montar | 60 · i |
| Filtros | Chips | y 10 → 0 escalonado; al filtrar las tarjetas se reordenan con layout | montar / clic | 40 · i |
| Retícula | 9 tarjetas | opacity con y 24 → 0, escalonado por fila | viewport | 80 · i |
| Retícula | Tarjeta en hover | Imagen scale 1.04, flecha rota 45°, dominio a ámbar | hover | — |
| Banda | Bloque completo | y 24 → 0 con opacity | viewport | 0 |
| 03 — Proyecto | ||||
| Cabecera | Nombre 104 px | Máscara vertical: y 100% → 0 | montar | 60 |
| Cabecera | Tabla de datos | Filas escalonadas con y 12 → 0 | montar | 200 + 50 · i |
| Imagen hero | A sangre 600 px | scale 1.06 → 1 más parallax al hacer scroll | montar + scroll | 0 |
| Cuerpo | Párrafos | y 18 → 0 con opacity | viewport | 60 · i |
| Métricas | 4 cifras | Conteo numérico de 0 al valor en 1.2 s | viewport | 100 · i |
| Capacidades | 4 columnas | Icono scale 0.8 → 1 y texto y 14 → 0 | viewport | 80 · i |
| Par de imágenes | A y B | clip-path reveal lateral, alternando la dirección | viewport | 0 / 140 |
| Cita | Bloque completo | Máscara por línea | viewport | 0 |
| Siguiente | Flecha ámbar | hover: scale 1.08 con x + 4 px, con muelle | hover | — |
| 04 — Nosotros | ||||
| Cabecera | Titular | Máscara por línea | montar | 70 · i |
| Imagen equipo | A sangre | scale 1.06 → 1 más parallax | montar + scroll | 0 |
| Trayectoria | 8 entradas | y 20 → 0; el año entra 80 ms antes que su texto | viewport | 70 · i |
| Dirección | 4 retratos | clip-path reveal de abajo a arriba, escalonado | viewport | 90 · i |
| Cultura | Fondo de página | Crossfade tinta → marfil | scroll | — |
| Cultura | 4 reglas | y 18 → 0 con opacity | viewport | 80 · i |
| Vacantes | 4 filas | y 14 → 0; en hover fondo y flecha x + 6 px | viewport / hover | 60 · i |
| Contacto | Halo ámbar | opacity 0 → 1 con scale 0.9 → 1 | viewport | 0 |
| Contacto | 3 oficinas | y 16 → 0 escalonado | viewport | 70 · i |
| Global | ||||
| Todas | Cambio de página | Salida opacity → 0 con y −12 en 200 ms; entrada opacity con y 12 → 0 en 400 ms | cambio de ruta | — |
| Todas | Botones | hover: scale 1.02 y aclarado; al pulsar scale 0.98 | hover / tap | — |
| Todas | Flechas ↗ | hover: rotación de 45° con muelle | hover | — |
| Todas | Cursor | Efecto magnético con factor 0.15 sobre botones y tarjetas | hover | — |
| Todas | Reduced motion | Se anulan y, scale y parallax; solo queda opacity a 200 ms | preferencia del SO | — |
Implementación
Las cuatro piezas de código que lo sostienen todo.
Con estos cuatro archivos se cubre el 90 % de la tabla anterior.
export const EASE = {
enter: [0.16, 1, 0.3, 1],
text: [0.25, 1, 0.5, 1],
page: [0.65, 0, 0.35, 1],
} as const
export const DUR = {
enter: 0.7,
text: 0.5,
page: 0.4,
}
export const SPRING = {
type: 'spring',
stiffness: 220,
damping: 30,
}
export const enter = {
hidden: { opacity: 0, y: 24 },
show: {
opacity: 1,
y: 0,
transition: { duration: DUR.enter, ease: EASE.enter },
},
}
export const stagger = (step = 0.08, delay = 0) => ({
show: {
transition: { staggerChildren: step, delayChildren: delay },
},
})
export const lineMask = {
hidden: { y: '100%' },
show: {
y: 0,
transition: { duration: DUR.text, ease: EASE.text },
},
}
'use client'
import { motion, useReducedMotion } from 'framer-motion'
import { enter, fadeOnly } from '@/lib/variants'
export function Reveal({ children, delay = 0 }) {
const reduce = useReducedMotion()
return (
<motion.div
variants={reduce ? fadeOnly : enter}
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.4 }}
transition={{ delay }}
>
{children}
</motion.div>
)
}
export function MaskedHeading({ lines }) {
return (
<h1>
{lines.map((line, i) => (
<span key={i} className="block overflow-hidden">
<motion.span
className="block"
variants={lineMask}
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.5 }}
transition={{ delay: i * 0.08 }}
>
{line}
</motion.span>
</span>
))}
</h1>
)
}
Accesibilidad
Accesibilidad y rendimiento.
Si alguna de estas cinco reglas se rompe, la animación se quita. Ninguna vale una caída de rendimiento ni un usuario mareado.
-
Se respeta prefers-reduced-motion
Con la preferencia activa se eliminan y, scale y parallax. Queda solo un fundido de 200 ms para que la jerarquía siga leyéndose.
-
Solo transform y opacity
Nunca se anima width, height, top ni left: fuerzan recálculo de layout. Todo movimiento se resuelve con translate y scale en la GPU.
-
will-change acotado
Se aplica al entrar en el viewport y se retira al terminar. Dejarlo permanente reserva memoria de vídeo sin motivo.
-
Sin parallax en móvil
Por debajo de 768 px se desactivan parallax y efecto magnético del cursor. En táctil no aportan y cuestan batería.
-
La tipografía carga antes
Las máscaras de titular esperan a document.fonts.ready. Animar antes deja ver el salto de fuente.