옵션
집집 Skill 웹 개발 motion-ui

motion-ui

affaan-m/ECC affaan-m/ECC

React/Next.js를 위한, 성능, 접근성 및 사용성 지침을 갖춘 프로덕션급 UI 모션 시스템을 제공합니다.

...모든 것을 확장하십시오
0
업데이트 된 시간 2026년 10월 1일

모션 시스템 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 && (
        
          
            
            
      )}
    
  )
}

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

  return (
    <>
      

SSR 안전성

  • 서버 렌더링과 클라이언트 렌더링 간의 초기 상태를 일치시킵니다
  • 암시적인 애니메이션 시작점을 피하십시오(항상 initial 명시적으로 설정)
  • 모션 컴포넌트를 "use client" Next.js 앱 라우터

디버깅

확인 사항:

  • 잘못된 임포트 (혼용 motion/react 및 framer-motion)
  • 누락된 "use client" 지시어가 누락됨)
  • 누락된 key prop이 누락됨 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 
}
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" />
}
```

모든 파일

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

복사 복사
빠른 설정: 스킬 폴더를 .claude/skills/로 복사하세요. Claude가 해당 스킬을 자동으로 감지하여 사용합니다.
저장소 affaan-m/ECC

관련 스킬

github-code-search
업데이트 된 시간 2026년 6월 29일
drizzle-orm
업데이트 된 시간 2026년 6월 29일
prisma-client-api
업데이트 된 시간 2026년 6월 29일
clickhouse-io
업데이트 된 시간 2026년 6월 29일
OR