Saltar al contenido

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.

lib/motion.ts
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,
}
lib/variants.ts
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 },
  },
}
components/Reveal.tsx
'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>
  )
}
components/MaskedHeading.tsx
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.