motion-ui
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 todoMotion 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 utilizatransformpara 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 coordinarlaslayoutIdsolo 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éasemodelas 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 && (
Dialog Title
)}
)
}
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
initialde 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/reactyframer-motion) - falta
"use client"en el enrutador de la aplicación Next.js - Falta la
keyprop enAnimatePresencelos elementos secundarios - Discrepancia en la hidratación (el estado inicial difiere entre SSR y el cliente)
layoutUso 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 modeConfigurado 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
layouten contenedores grandes o que ocupan toda la ventana gráfica - Omisión de
modeenAnimatePresence(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
}
---
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 archivosInstalar motion-ui
Descarga y descomprime los archivos de habilidades en tu directorio .claude/skills/.
Descargar ZIPClona 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





Hogar
