motion-ui
affaan-m/ECC
React/Next.js를 위한, 성능, 접근성 및 사용성 지침을 갖춘 프로덕션급 UI 모션 시스템을 제공합니다.
...모든 것을 확장하십시오모션 시스템 v4.2
React / Next.js용, 실제 배포에 바로 사용할 수 있는 UI 모션 시스템.
장식적인 요소보다는 성능, 접근성, 사용 편의성에 중점을 두었습니다.
사용 시점
다음과 같은 경우 이 모션 시스템을 사용하세요:
- 사용자의 주의를 유도할 때 (예: 온보딩, 주요 동작)
- 상태를 전달할 때 (로딩, 성공, 오류, 전환)
- 공간적 연속성을 유지할 때 (레이아웃 변경, 탐색)
적합한 시나리오
- 상호작용 요소(버튼, 모달, 메뉴)
- 상태 전환 (로딩 → 로딩 완료, 열림 → 닫힘)
- 탐색 및 레이아웃 연속성 (공유 요소, 크로스페이드)
고려 사항
- 접근성: 항상 동작 축소를 지원해야 합니다
- 기기 적응: 저사양 기기에 맞게 조정
- 성능 상의 절충: 시각적 부드러움보다 반응성을 우선시
다음과 같은 경우에는 모션 사용을 피하십시오
- 순전히 장식적인 목적인 경우
- 사용성이나 명확성을 저해할 때
- 성능에 부정적인 영향을 미치는 경우
작동 원리
핵심 원칙
모션은 반드시 다음을 충족해야 합니다:
- 주의를 유도해야 합니다
- 상태를 전달해야 합니다
- 공간적 연속성을 유지해야 합니다
이 중 하나도 충족하지 않는다면 → 제거하십시오.
설치
npm install motion
버전
motion/react- 현재 Motion for React 프로젝트의 기본값 (패키지:motion)framer-motion- 여전히 Framer Motion에 의존하는 프로젝트를 위한 레거시 임포트 경로
혼용하지 마십시오. 혼용 시 내부 스케줄러 간 충돌이 발생하고 AnimatePresence 컨텍스트가 손상됩니다. 즉, 한 패키지의 컴포넌트가 다른 패키지의 컴포넌트와 종료 애니메이션을 조율하지 못합니다.
프로젝트에서 사용하는 버전을 확인하려면:
cat package.json | grep -E '"motion"|"framer-motion"'
항상 하나의 소스에서 일관되게 임포트하십시오:
// Correct (modern)
import { motion, AnimatePresence } from "motion/react"
// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"
// Never mix both in the same project
모션 토큰
// 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
}
}
사용 예시:
import { motionTokens } from "@/lib/motionTokens"
성능 규칙
안전
- 변환
- opacity
피해야 할 사항
- 너비 / 높이
- 상단 / 좌측
규칙: 반응성 > 부드러움
기기 적응
이 휴리스틱은 CPU 코어 수와 사용 가능한 메모리를 결합하여 더 신뢰할 수 있는 신호를 제공합니다. deviceMemory Chrome/Android에서 사용할 수 있으며, 대체 기능은 Safari와 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
접근성
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
아키텍처 및 패턴
핵심 패턴
| 시나리오 | 패턴 |
|---|---|
| 호버 피드백 | whileHover |
| 탭/누름 피드백 | whileTap |
| 스크롤 시 표시 | whileInView |
| 스크롤 연동 값 | useScroll + useTransform |
| 조건부 마운트/언마운트 | AnimatePresence |
| 작은 레이아웃 이동 (단일 요소, ~300px 미만의 변화) | layout prop |
| 크게 레이아웃이 바뀌거나 전체 페이지가 재구성되는 경우 | 피해야 함 layout; 대신 CSS 전환이나 페이지 단위 라우팅을 사용하십시오 |
| 복잡하고 명령형인 시퀀스 | useAnimate |
큰 컨테이너에서 `
layout`를 피해야 하는 이유는 무엇일까요? Framer의 레이아웃 애니메이션은transform를 사용하여 위치를 조정하지만, 뷰포트 전체를 차지하거나 심층적인 리플로우를 유발하는 요소의 경우, 측정 비용으로 인해 눈에 띄는 끊김 현상과 CLS가 발생합니다. CSS 그리드/플렉스박스 전환을 선호하거나layoutId특정 자식 요소에만 적용하십시오.
레이아웃 및 전환
- 공유된 요소 전환 →
layoutId(마운트된 인스턴스마다 고유해야 함) - 진입/이탈 전환 →
AnimatePresence(아래mode아래 지침 참조)
AnimatePresence mode
항상 mode 명시적으로 지정해야 합니다 — 기본값("sync")에서는 진입과 퇴장이 동시에 실행되어 대부분의 UI 패턴에서 시각적 중복 현상이 발생합니다.
mode |
사용 시점 |
|---|---|
"wait" |
‘Exit’가 완료된 후에야 ‘enter’가 시작됩니다. 모달, 토스트, 페이지 전환에 사용하십시오. |
"sync" (기본값) |
‘enter’와 ‘exit’가 겹칩니다. 겹침이 의도된 경우(예: 크로스페이드 캐러셀)에만 사용하십시오. |
"popLayout" |
종료되는 요소는 흐름에서 즉시 빠져나가고, 나머지 항목들이 애니메이션을 통해 그 공간을 채웁니다. 목록, 탭, 닫을 수 있는 카드에 사용합니다. |
// Modal — always use "wait"
{open && }
// Dismissible list item — use "popLayout"
{items.map(item => )}
고급 패턴 (개념)
- 파라락스(스크롤 연동 변환)
- 스크롤 스토리텔링 (고정 섹션)
- 3D 틸트(포인터 기반 변환)
- 크로스페이드 (공유
layoutId) - 점진적 노출 (클립 경로)
- 스켈레톤 로딩 (반복되는 불투명도)
- 마이크로 인터랙션(호버/탭 피드백)
- 스프링 시스템 (물리 기반 모션)
모달 필수 요소
- 포커스 트랩
- 닫기(Esc)
- 스크롤 잠금
- ARIA 역할
- 사용법
AnimatePresence mode="wait"다음 모달이 표시되기 전에 종료 애니메이션이 완료되도록
전체 예제
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 (
<>
SSR 안전성
- 서버 렌더링과 클라이언트 렌더링 간의 초기 상태를 일치시킵니다
- 암시적인 애니메이션 시작점을 피하십시오(항상
initial명시적으로 설정) - 모션 컴포넌트를
"use client"Next.js 앱 라우터
디버깅
확인 사항:
- 잘못된 임포트 (혼용
motion/react및framer-motion) - 누락된
"use client"지시어가 누락됨) - 누락된
keyprop이 누락됨AnimatePresence자식 요소 - 하이드레이션 불일치 (SSR과 클라이언트 간 초기 상태가 다름)
layout대용량 컨테이너에서 prop 오용으로 인한 리플로우 지연- 상태 기반 애니메이션이 트리거되지 않음 (의존성 배열 확인)
QA
- CLS 없음
- 키보드 작동
- 모달 창에서 포커스가 갇힘
- ARIA 역할이 올바름 (
role="dialog",aria-modal="true") - 움직임 최소화 규칙 준수 (
useReducedMotion+ CSS 미디어 쿼리) - Next.js에서 하이드레이션 경고 없음
- 언마운트 시 애니메이션이 깔끔하게 중지됨(메모리 누수 없음)
AnimatePresence mode모든 사용 위치에서 명시적으로 설정
안티패턴
- 레이아웃 속성에 애니메이션 적용 (
width,height,top,left) - 목적 없는 무한 애니메이션 (항상 자문해 보세요: 이것이 어떤 상태를 전달하는가?)
- 목록의 간격이 지나치게 넓게 배치됨 (
staggerChildren≤ 0.1초; 그 이상이면 느리게 느껴짐) - 모션 최소화 설정을 무시하는 것
- 다음과 같이 사용
layout대형 또는 전체 뷰포트 컨테이너에서 - 생략
mode를AnimatePresence(기본값"sync"를 생략하면 시각적 중복 현상이 발생합니다) - 순전히 장식 목적으로 모션 사용
철학
모션은 상호작용 디자인입니다.
최종 원칙
모션이 사용자 경험(UX)을 개선하지 않는다면 → 제거하십시오.
예시
버튼 상호작용
import { motion } from "motion/react"
export function Button() {
return (
Click me
)
}
모션 축소 예시
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
)
}
계단식 목록
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}
))}
)
}
AnimatePresence가 적용된 모달
import { motion, AnimatePresence } from "motion/react"
export function Modal({ open }: { open: boolean }) {
return (
{open && (
)}
)
}
스크롤 패럴랙스
import { useScroll, useTransform, motion } from "motion/react"
export function Parallax() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -80])
return
}
스켈레톤 로딩
import { motion } from "motion/react"
export function Skeleton() {
return (
)
}
공유 레이아웃 (크로스페이드)
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" />
}
```
모든 파일
1개 파일motion-ui 설치
스킬 파일을 다운로드하여 .claude/skills/ 디렉터리에 압축을 풀어주세요.
ZIP 다운로드저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.
git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-ui # Copy SKILL.md to your .claude/skills/ directory
복사





집
