opción

motion-ui

affaan-m/ECC affaan-m/ECC

Ofrece un sistema de animación de la interfaz de usuario listo para producción para React/Next.js, con directrices sobre rendimiento, accesibilidad y usabilidad.

...Expandir todo
0
Tiempo actualizado 1 de octubre de 2026

Motion System v4.2

Sistema de animación de la interfaz de usuario listo para producción para React / Next.js.

Centrado en el rendimiento, la accesibilidad y la usabilidad, no en la decoración.

Cuándo utilizarlo

Utiliza este sistema de animación cuando el movimiento:

  • Guíe la atención (p. ej., en la incorporación de nuevos usuarios o en acciones clave)
  • Comunican un estado (carga, éxito, error, transiciones)
  • Preserve la continuidad espacial (cambios de diseño, navegación)

Escenarios adecuados

  • Componentes interactivos (botones, ventanas modales, menús)
  • Transiciones de estado (cargando → cargado, abierto → cerrado)
  • Continuidad en la navegación y el diseño (elementos compartidos, fundido cruzado)

Consideraciones

  • Accesibilidad: admite siempre el modo de movimiento reducido
  • Adaptación a los dispositivos: ajustar para dispositivos de gama baja
  • Compromisos de rendimiento: dar prioridad a la capacidad de respuesta frente a la fluidez visual

Evita utilizar movimiento cuando

  • Sea puramente decorativo
  • Reduzca la usabilidad o la claridad
  • Afecta negativamente al rendimiento

Cómo funciona

Principio fundamental

El movimiento debe:

  • Dirigir la atención
  • Comunicar el estado
  • Preservar la continuidad espacial

Si no cumple ninguno de ellos → elimínalo.

Instalación

npm install motion

Versión

  • motion/react - predeterminada para los proyectos actuales de Motion for React (paquete: motion)
  • framer-motion - ruta de importación heredada para proyectos que aún dependen de Framer Motion

No las mezcles. La mezcla provoca conflictos entre los programadores internos y la ruptura de AnimatePresence : los componentes de un paquete no coordinarán las animaciones de salida con los componentes del otro.

Para comprobar qué versión utiliza tu proyecto:

cat package.json | grep -E '"motion"|"framer-motion"'

Importa siempre desde una misma fuente de forma coherente:

// Correct (modern)
import { motion, AnimatePresence } from "motion/react"

// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"

// Never mix both in the same project

Tokens de Motion

// motionTokens.ts
export const motionTokens = {
  duration: {
    fast: 0.18,
    normal: 0.35,
    slow: 0.6
  },
  // Use these as the `ease` value inside a `transition` object:
  // transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
  easing: {
    smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
    sharp:  [0.4,  0, 0.2, 1] as [number, number, number, number]
  },
  distance: {
    sm: 8,
    md: 16,
    lg: 24
  }
}

Ejemplo de uso:

import { motionTokens } from "@/lib/motionTokens"


Reglas de rendimiento

Seguro

  • transformar
  • opacidad

Evita

  • ancho / alto
  • arriba / izquierda

Regla: adaptabilidad > fluidez

Adaptación al dispositivo

Esta heurística combina el número de núcleos de la CPU y la memoria disponible para obtener una señal más fiable. deviceMemory Está disponible en Chrome/Android; la alternativa cubre Safari y Firefox.

const isLowEnd =
  typeof navigator !== "undefined" && (
    // Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
    (navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
    // Few cores AND no memory API (covers Safari/Firefox on weak hardware)
    (navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
  )

const duration = isLowEnd ? 0.2 : 0.4

Accesibilidad

JS (useReducedMotion)

import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    
  )
}

CSS

@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition {
    transition: opacity 0.2s;
  }

  .motion-reduce-transform {
    transform: none !important;
  }
}

Tailwind

Arquitectura y patrones

Patrones básicos

Escenario Patrón
Respuesta al pasar el cursor whileHover
Respuesta al toque o pulsación whileTap
Mostrar al desplazarse whileInView
Valor vinculado al desplazamiento useScroll + useTransform
Montaje/desmontaje condicional AnimatePresence
Pequeños cambios en el diseño (un solo elemento, cambio < ~300 px) layout prop
Cambios importantes en el diseño o reajustes de página completa Evitar layout; utiliza en su lugar transiciones CSS o enrutamiento a nivel de página
Secuencias complejas e imperativas useAnimate

¿Por qué evitar «layout» en contenedores grandes? La animación de diseño de Framer utiliza transform para ajustar las posiciones, pero en elementos que abarcan toda la ventana gráfica o que provocan un reajuste profundo, el coste de las mediciones provoca tirones visibles y CLS. Es preferible utilizar transiciones CSS Grid/Flexbox o coordinarlas layoutId solo en elementos hijos específicos.

Diseño y transiciones

  • Transiciones compartidas entre elementos → layoutId (deben ser únicas por cada instancia montada)
  • Transiciones de entrada/salida → AnimatePresence (véase mode las instrucciones que figuran a continuación)

AnimatePresence mode

Especifica siempre mode de forma explícita: la configuración predeterminada ("sync") ejecuta la entrada y la salida simultáneamente, lo que provoca un solapamiento visual en la mayoría de los patrones de interfaz de usuario.

mode Cuándo utilizarlo
"wait" La salida se completa antes de que comience la entrada. Úsalo para ventanas modales, notificaciones emergentes y transiciones de página.
"sync" (por defecto) La entrada y la salida se solapan. Úsalo solo cuando el solapamiento sea intencionado (p. ej., carruseles con fundido cruzado).
"popLayout" El elemento que sale se retira del flujo inmediatamente; los elementos restantes se animan para ocupar el espacio. Úsalo para listas, pestañas y tarjetas que se pueden cerrar.
// Modal — always use "wait"

  {open && }


// Dismissible list item — use "popLayout"

  {items.map(item => )}

Patrones avanzados (conceptos)

  • Paralaje (transformaciones vinculadas al desplazamiento)
  • Narrativa mediante desplazamiento (secciones fijas)
  • Inclinación 3D (transformaciones basadas en el puntero)
  • Fundido cruzado (compartido layoutId)
  • Revelación progresiva (trayectoria de recorte)
  • Carga esquelética (opacidad en bucle)
  • Microinteracciones (respuesta al pasar el cursor o al tocar)
  • Sistema de resorte (movimiento basado en la física)

Fundamentos de los modales

  • Trampa de enfoque
  • Cerrar ventana modal
  • Bloqueo de desplazamiento
  • Roles ARIA
  • Uso AnimatePresence mode="wait" para que la animación de salida finalice antes de que aparezca el siguiente modal

Ejemplo completo

import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"

function useFocusTrap(ref: React.RefObject, active: boolean) {
  useEffect(() => {
    if (!active || !ref.current) return
    const el = ref.current
    const focusable = el.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last  = focusable[focusable.length - 1]

    function handleKey(e: KeyboardEvent) {
      if (e.key !== "Tab") return
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault()
        last?.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault()
        first?.focus()
      }
    }

    el.addEventListener("keydown", handleKey)
    first?.focus()
    return () => el.removeEventListener("keydown", handleKey)
  }, [active, ref])
}

function useScrollLock(active: boolean) {
  useEffect(() => {
    if (!active) return
    const prev = document.body.style.overflow
    document.body.style.overflow = "hidden"
    return () => { document.body.style.overflow = prev }
  }, [active])
}

function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
  const ref = useRef(null)

  useFocusTrap(ref, open)
  useScrollLock(open)

  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") closeModal()
    }
    if (open) window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [open, closeModal])

  return (
    // mode="wait" ensures exit animation finishes before any new modal enters
    
      {open && (
        
          
            
            
      )}
    
  )
}

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      

Seguridad del SSR

  • Hacer coincidir los estados iniciales entre las representaciones del servidor y del cliente
  • Evita los orígenes de animación implícitos (establece siempre initial de forma explícita)
  • Envuelve los componentes de movimiento en "use client" el enrutador de aplicaciones de Next.js

Depuración

Comprueba:

  • Importación incorrecta (mezcla de motion/react y framer-motion)
  • falta "use client" en el enrutador de la aplicación Next.js
  • Falta la key prop en AnimatePresence los elementos secundarios
  • Discrepancia en la hidratación (el estado inicial difiere entre SSR y el cliente)
  • layout Uso incorrecto de la propiedad en contenedores grandes que provoca tirones en el rediseño
  • La animación basada en el estado no se activa (comprueba las matrices de dependencias)

Control de calidad

  • Sin CLS
  • El teclado funciona
  • El foco queda atrapado en las ventanas modales
  • Los roles ARIA son correctos (role="dialog", aria-modal="true")
  • Se respeta la reducción de movimiento (useReducedMotion + consulta de medios CSS)
  • No hay advertencias de hidratación en Next.js
  • Las animaciones se detienen correctamente al desmontarse (sin fugas de memoria)
  • AnimatePresence mode Configurado explícitamente en todos los puntos de uso

Antipatrones

  • Animación de propiedades de diseño (width, height, top, left)
  • Animaciones infinitas sin propósito (pregúntate siempre: ¿qué estado transmite esto?)
  • Exceso de escalonamiento en las listas (mantén staggerChildren ≤ 0,1 s; más allá de eso, se percibe como lento)
  • Ignorar las preferencias de movimiento reducido
  • Uso de layout en contenedores grandes o que ocupan toda la ventana gráfica
  • Omisión de mode en AnimatePresence (el valor por defecto "sync" provoca solapamientos visuales)
  • Utilizar el movimiento únicamente con fines decorativos

Filosofía

El movimiento es diseño de interacción.

Regla definitiva

Si el movimiento no mejora la experiencia de usuario (UX), elimínalo.

Ejemplos

Interacción con botones

import { motion } from "motion/react"

export function Button() {
  return (
    
      Click me
    
  )
}

Ejemplo de movimiento reducido

import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    
  )
}

Lista escalonada

import { motion } from "motion/react"

const container = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
  }
}

const item = {
  hidden:  { opacity: 0, y: 10 },
  visible: { opacity: 1, y: 0,  transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}

export function List() {
  return (
    
      {[1, 2, 3].map(i => (
        Item {i}
      ))}
    
  )
}

Ventana modal con AnimatePresence

import { motion, AnimatePresence } from "motion/react"

export function Modal({ open }: { open: boolean }) {
  return (
    
      {open && (
        
      )}
    
  )
}

Desplazamiento con efecto de paralaje

import { useScroll, useTransform, motion } from "motion/react"

export function Parallax() {
  const { scrollYProgress } = useScroll()
  const y = useTransform(scrollYProgress, [0, 1], [0, -80])

  return 
}

Carga esquelética

import { motion } from "motion/react"

export function Skeleton() {
  return (
    
  )
}

Diseño compartido (fundido cruzado)

import { motion } from "motion/react"

// layoutId must be unique per mounted instance.
// If multiple instances can exist simultaneously, append a unique id:
// layoutId={`shared-${item.id}`}
export function Shared() {
  return 
}
Ver en GitHub
---
name: motion-ui
description: Provides a production-ready UI motion system for React/Next.js with performance, accessibility, and usability guidelines.
---

# Motion System v4.2

Production-ready UI motion system for React / Next.js.

Focused on **performance, accessibility, and usability** — not decoration.

## When to Use

Use this motion system when motion:

* Guides attention (e.g., onboarding, key actions)
* Communicates state (loading, success, error, transitions)
* Preserves spatial continuity (layout changes, navigation)

### Appropriate Scenarios

* Interactive components (buttons, modals, menus)
* State transitions (loading → loaded, open → closed)
* Navigation and layout continuity (shared elements, crossfade)

### Considerations

* **Accessibility**: Always support reduced motion
* **Device adaptation**: Adjust for low-end devices
* **Performance trade-offs**: Prefer responsiveness over visual smoothness

### Avoid Using Motion When

* It is purely decorative
* It reduces usability or clarity
* It impacts performance negatively

---

## How It Works

### Core Principle

Motion must:

* Guide attention
* Communicate state
* Preserve spatial continuity

If it does none → remove it.

---

### Installation

```bash
npm install motion
```

---

### Version

* `motion/react` - default for current Motion for React projects (package: `motion`)
* `framer-motion` - legacy import path for projects that still depend on Framer Motion

**Do not mix.** Mixing causes conflicting internal schedulers and broken `AnimatePresence` contexts — components from one package will not coordinate exit animations with components from the other.

To check which version your project uses:

```bash
cat package.json | grep -E '"motion"|"framer-motion"'
```

Always import from one source consistently:

```ts
// Correct (modern)
import { motion, AnimatePresence } from "motion/react"

// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"

// Never mix both in the same project
```

---

### Motion Tokens

```ts
// motionTokens.ts
export const motionTokens = {
  duration: {
    fast: 0.18,
    normal: 0.35,
    slow: 0.6
  },
  // Use these as the `ease` value inside a `transition` object:
  // transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
  easing: {
    smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
    sharp:  [0.4,  0, 0.2, 1] as [number, number, number, number]
  },
  distance: {
    sm: 8,
    md: 16,
    lg: 24
  }
}
```

Usage example:

```tsx
import { motionTokens } from "@/lib/motionTokens"

<motion.div
  initial={{ opacity: 0, y: motionTokens.distance.md }}
  animate={{ opacity: 1, y: 0 }}
  transition={{
    duration: motionTokens.duration.normal,
    ease: motionTokens.easing.smooth
  }}
/>
```

---

### Performance Rules

**Safe**

* transform
* opacity

**Avoid**

* width / height
* top / left

Rule: responsiveness > smoothness

---

### Device Adaptation

The heuristic combines CPU core count **and** available memory for a more reliable signal. `deviceMemory` is available on Chrome/Android; the fallback covers Safari and Firefox.

```ts
const isLowEnd =
  typeof navigator !== "undefined" && (
    // Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
    (navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
    // Few cores AND no memory API (covers Safari/Firefox on weak hardware)
    (navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
  )

const duration = isLowEnd ? 0.2 : 0.4
```

---

### Accessibility

#### JS (useReducedMotion)

```tsx
import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: reduce ? 0 : 24 }}
      animate={{ opacity: 1, y: 0 }}
    />
  )
}
```

#### CSS

```css
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition {
    transition: opacity 0.2s;
  }

  .motion-reduce-transform {
    transform: none !important;
  }
}
```

#### Tailwind

```html
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
```

---

### Architecture & Patterns

#### Core Patterns

| Scenario | Pattern |
|---|---|
| Hover feedback | `whileHover` |
| Tap / press feedback | `whileTap` |
| Reveal on scroll | `whileInView` |
| Scroll-linked value | `useScroll` + `useTransform` |
| Conditional mount/unmount | `AnimatePresence` |
| Small layout shifts (single element, < ~300px change) | `layout` prop |
| Large layout shifts or full-page reflows | Avoid `layout`; use CSS transitions or page-level routing instead |
| Complex, imperative sequences | `useAnimate` |

> **Why avoid `layout` on large containers?** Framer's layout animation uses `transform` to reconcile positions, but on elements that span the full viewport or trigger deep reflow, the measurement cost causes visible jank and CLS. Prefer CSS Grid/Flexbox transitions or coordinate with `layoutId` on specific child elements only.

#### Layout & Transitions

* Shared element transitions → `layoutId` (must be unique per mounted instance)
* Enter / exit transitions → `AnimatePresence` (see `mode` guidance below)

#### AnimatePresence `mode`

Always specify `mode` explicitly — the default (`"sync"`) runs enter and exit simultaneously, which causes visual overlap in most UI patterns.

| `mode` | When to use |
|---|---|
| `"wait"` | Exit completes before enter starts. Use for **modals, toasts, page transitions**. |
| `"sync"` (default) | Enter and exit overlap. Use only when overlap is intentional (e.g., crossfade carousels). |
| `"popLayout"` | Exiting element is popped out of flow immediately; remaining items animate to fill. Use for **lists, tabs, dismissible cards**. |

```tsx
// Modal — always use "wait"
<AnimatePresence mode="wait">
  {open && <Modal key="modal" />}
</AnimatePresence>

// Dismissible list item — use "popLayout"
<AnimatePresence mode="popLayout">
  {items.map(item => <Card key={item.id} />)}
</AnimatePresence>
```

---

### Advanced Patterns (Concepts)

* Parallax (scroll-linked transforms)
* Scroll storytelling (sticky sections)
* 3D tilt (pointer-based transforms)
* Crossfade (shared `layoutId`)
* Progressive reveal (clip-path)
* Skeleton loading (looped opacity)
* Micro-interactions (hover/tap feedback)
* Spring system (physics-based motion)

---

### Modal Essentials

* Focus trap
* Escape close
* Scroll lock
* ARIA roles
* Use `AnimatePresence mode="wait"` so exit animation completes before the next modal enters

#### Full Example

```tsx
import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"

function useFocusTrap(ref: React.RefObject<HTMLDivElement | null>, active: boolean) {
  useEffect(() => {
    if (!active || !ref.current) return
    const el = ref.current
    const focusable = el.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last  = focusable[focusable.length - 1]

    function handleKey(e: KeyboardEvent) {
      if (e.key !== "Tab") return
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault()
        last?.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault()
        first?.focus()
      }
    }

    el.addEventListener("keydown", handleKey)
    first?.focus()
    return () => el.removeEventListener("keydown", handleKey)
  }, [active, ref])
}

function useScrollLock(active: boolean) {
  useEffect(() => {
    if (!active) return
    const prev = document.body.style.overflow
    document.body.style.overflow = "hidden"
    return () => { document.body.style.overflow = prev }
  }, [active])
}

function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
  const ref = useRef<HTMLDivElement>(null)

  useFocusTrap(ref, open)
  useScrollLock(open)

  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") closeModal()
    }
    if (open) window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [open, closeModal])

  return (
    // mode="wait" ensures exit animation finishes before any new modal enters
    <AnimatePresence mode="wait">
      {open && (
        <motion.div
          role="dialog"
          aria-modal="true"
          aria-labelledby="modal-title"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.2 }}
          className="fixed inset-0 flex items-center justify-center bg-black/40"
        >
          <motion.div
            ref={ref}
            initial={{ scale: 0.95, opacity: 0 }}
            animate={{ scale: 1,    opacity: 1 }}
            exit={{    scale: 0.95, opacity: 0 }}
            transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
            className="bg-white p-6 rounded"
          >
            <h2 id="modal-title">Dialog Title</h2>
            <button onClick={closeModal}>Close</button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      <Modal open={open} closeModal={() => setOpen(false)} />
    </>
  )
}
```

---

### SSR Safety

* Match initial states between server and client renders
* Avoid implicit animation origins (always set `initial` explicitly)
* Wrap motion components in `"use client"` in Next.js App Router

---

### Debugging

Check:

* Wrong import (mixing `motion/react` and `framer-motion`)
* Missing `"use client"` directive in Next.js App Router
* Missing `key` prop on `AnimatePresence` children
* Hydration mismatch (initial state differs between SSR and client)
* `layout` prop misuse on large containers causing reflow jank
* State-driven animation not triggering (check dependency arrays)

---

### QA

* No CLS
* Keyboard works
* Focus trapped in modals
* ARIA roles correct (`role="dialog"`, `aria-modal="true"`)
* Reduced motion respected (`useReducedMotion` + CSS media query)
* No hydration warnings in Next.js
* Animations stop cleanly on unmount (no memory leaks)
* `AnimatePresence mode` set explicitly on all usage sites

---

### Anti-Patterns

* Animating layout properties (`width`, `height`, `top`, `left`)
* Infinite animations without purpose (always ask: what state does this communicate?)
* Over-staggering lists (keep `staggerChildren` ≤ 0.1s; beyond that it feels slow)
* Ignoring reduced motion preferences
* Using `layout` on large or full-viewport containers
* Omitting `mode` on `AnimatePresence` (default `"sync"` causes visual overlap)
* Using motion purely for decoration

---

### Philosophy

Motion is interaction design.

---

### Final Rule

> If motion does not improve UX → remove it.

---

## Examples

### Button Interaction

```tsx
import { motion } from "motion/react"

export function Button() {
  return (
    <motion.button
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.97 }}
      transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
    >
      Click me
    </motion.button>
  )
}
```

---

### Reduced Motion Example

```tsx
import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: reduce ? 0 : 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: reduce ? 0.1 : 0.35, ease: [0.22, 1, 0.36, 1] }}
    />
  )
}
```

---

### Stagger List

```tsx
import { motion } from "motion/react"

const container = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
  }
}

const item = {
  hidden:  { opacity: 0, y: 10 },
  visible: { opacity: 1, y: 0,  transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}

export function List() {
  return (
    <motion.ul variants={container} initial="hidden" animate="visible">
      {[1, 2, 3].map(i => (
        <motion.li key={i} variants={item}>Item {i}</motion.li>
      ))}
    </motion.ul>
  )
}
```

---

### Modal with AnimatePresence

```tsx
import { motion, AnimatePresence } from "motion/react"

export function Modal({ open }: { open: boolean }) {
  return (
    <AnimatePresence mode="wait">
      {open && (
        <motion.div
          initial={{ opacity: 0, scale: 0.95 }}
          animate={{ opacity: 1, scale: 1    }}
          exit={{    opacity: 0, scale: 0.95 }}
          transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
        />
      )}
    </AnimatePresence>
  )
}
```

---

### Scroll Parallax

```tsx
import { useScroll, useTransform, motion } from "motion/react"

export function Parallax() {
  const { scrollYProgress } = useScroll()
  const y = useTransform(scrollYProgress, [0, 1], [0, -80])

  return <motion.div style={{ y }} />
}
```

---

### Skeleton Loading

```tsx
import { motion } from "motion/react"

export function Skeleton() {
  return (
    <motion.div
      className="bg-gray-200 h-6 w-full rounded"
      animate={{ opacity: [0.5, 1, 0.5] }}
      transition={{
        duration: 1.5,       // comfortable pulse — was missing, caused fast flash
        repeat: Infinity,
        ease: "easeInOut"
      }}
    />
  )
}
```

---

### Shared Layout (Crossfade)

```tsx
import { motion } from "motion/react"

// layoutId must be unique per mounted instance.
// If multiple instances can exist simultaneously, append a unique id:
// layoutId={`shared-${item.id}`}
export function Shared() {
  return <motion.div layoutId="shared" />
}
```

Todos los archivos

1 archivos

Instalar motion-ui

Descarga y descomprime los archivos de habilidades en tu directorio .claude/skills/.

Descargar ZIP

Clona el repositorio y copia los archivos de la habilidad a tu proyecto.

git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-ui # Copy SKILL.md to your .claude/skills/ directory

Copiar Copiar
Configuración rápida: Copia la carpeta de la habilidad en .claude/skills/ Claude detectará y utilizará automáticamente la habilidad
Repositorio affaan-m/ECC

Habilidades relacionadas

github-code-search
Tiempo actualizado 29 de junio de 2026
drizzle-orm
Tiempo actualizado 29 de junio de 2026
prisma-client-api
Tiempo actualizado 29 de junio de 2026
clickhouse-io
Tiempo actualizado 29 de junio de 2026
OR