Communitygithub.com

motion-patterns

React / Next.js용 프로덕션 준비 애니메이션 패턴 — 버튼, 모달, 토스트, 스태거, 페이지 전환, 종료 애니메이션, 스크롤, 레이아웃. motion-foundations 토큰과 스프링으로 구축되었습니다.

motion-patterns란 무엇인가요?

Motion Patterns는 React 및 Next.js 애플리케이션에서 가장 일반적인 UI 애니메이션 요구사항을 해결하기 위해 설계된 복사-붙여넣기 방식의 애니메이션 패턴 모음입니다. 모든 패턴은 motion-foundations 라이브러리에서 제공하는 토큰과 스프링을 기반으로 하여 일관된 속도와 가속도를 보장합니다. 버튼, 카드, 모달, 토스트 알림, 스태거 목록, 페이지 전환(Next.js App Router), 스크롤 기반 효과, 확장 카드, 아코디언, 공유 요소 전환 등 다양한 UI 요소를 지원합니다. 이 스킬은 AnimatePresence를 통한 조건부 렌더링, useScroll과 useTransform을 활용한 스크롤 연동 애니메이션, layout과 layoutId를 사용한 레이아웃 애니메이션 패턴을 제공하며, 접근성과 SSR 안전성을 고려하여 설계되었습니다. 각 패턴은 명확한 규칙과 가이드라인을 따르며, 안티패턴을 식별하고 수정하는 방법도 함께 제공하여 일관되고 고품질의 애니메이션을 구현할 수 있도록 돕습니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/motion-patterns

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Motion Patterns

가장 일반적인 UI 애니메이션 요구사항을 위한 복사-붙여넣기 패턴입니다. 모든 패턴은 motion-foundations 토큰과 스프링을 기반으로 합니다. 새로운 duration이나 easing 값을 여기에 정의하지 말고 가져와서 사용하세요.

활성화 시점

  • 버튼, 카드, 모달, 토스트 알림 애니메이션
  • 스태거를 사용한 목록 진입 효과 구현
  • Next.js App Router에서 페이지 전환 설정
  • 조건부 콘텐츠의 진입/종료 애니메이션 추가
  • 스크롤 시 나타나는 효과, 스크롤 연결 진행률, 스티커 스토리 섹션 구현
  • 확장 카드, 아코디언, 공유 요소 전환 구현

산출물

이 스킬은 다음을 제공합니다:

  • 모든 표준 UI 컴포넌트에 대한 접근 가능하고 SSR에서 안전한 애니메이션
  • 올바른 종료 동작을 포함한 AnimatePresence로 감싸진 조건부 렌더링
  • Next.js App Router용 페이지 전환 래퍼 컴포넌트
  • useScroll + useTransform을 사용한 스크롤 시 나타나는 효과 및 스크롤 연결 패턴
  • 확장 및 크로스페이드 요소를 위한 레이아웃 애니메이션 패턴 (layout, layoutId)

원칙

  • 모든 패턴은 motion-foundations에서 가져옵니다. raw 숫자는 사용하지 않습니다.
  • 모든 조건부 렌더링은 key와 함께 AnimatePresence로 감싸집니다.
  • 종료 애니메이션은 항상 진입 애니메이션과 함께 정의되며, 나중에 추가로 생각하지 않습니다.
  • layout은 작고 고립된 변화에만 사용됩니다. 큰 하위 트리에는 명시적인 변환을 사용합니다.

규칙

  1. 조건부 렌더링을 항상 key와 함께 AnimatePresence로 감싸세요. 직접 자식에 key가 없으면 종료 애니메이션이 실행되지 않습니다.
  2. initial + animate를 정의할 때는 항상 exit도 정의하세요. 종료가 없는 애니메이션은 불완전합니다.
  3. 페이지 전환 시 mode="wait"를 사용하세요. 종료가 완료될 때까지 진입이 시작되어서는 안 됩니다.
  4. 자식이 약 5개 이상이거나 DOM 계층이 깊은 하위 트리에는 layout을 사용하지 마세요. 명시적인 x/y 변환을 대신 사용하세요.
  5. 스태거 간격은 0.05초에서 0.10초 사이여야 합니다. 그 이하는 기계처럼 느껴지고, 그 이상은 느리게 느껴집니다.
  6. 모달에는 반드시 다음이 포함되어야 합니다: 포커스 트랩, Escape 키 닫기, 스크롤 잠금, role="dialog", aria-modal="true".
  7. 스크롤 시 나타나는 효과는 viewport={{ once: true }}를 사용하세요. 스크롤 아웃 시 반복하는 것은 유익하기보다 산만합니다.
  8. 모든 토큰 값은 motion-foundations에서 가져옵니다. 인라인 숫자는 사용하지 않습니다.

결정 가이드

올바른 패턴 선택

상황패턴
요소가 나타나거나 사라짐AnimatePresence
항목이 순서대로 로딩되는 목록스태거 변형
라우트 간 이동페이지 전환 래퍼
요소가 제자리에서 크기 변경layout prop
동일한 요소가 페이지 컨텍스트 간 이동layoutId
스크롤하여 뷰에 들어올 때 요소가 나타남whileInView
스크롤 위치에 연결된 값useScroll + useTransform

mode="wait" vs mode="sync" 사용 시기

모드사용 시기
wait페이지 전환, 콘텐츠 교체 (한 번에 하나씩)
sync쌓인 알림, 목록 항목 (겹쳐도 괜찮은 경우)
popLayout재배치 목록에서 항목이 제거되는 경우

핵심 개념

AnimatePresence 계약

세 가지가 항상 참이어야 합니다:

  1. AnimatePresence가 조건문을 감싼다
  2. 직접 자식에 key가 있다
  3. 자식에 exit prop이 있다

이 중 하나라도 누락되면 종료 애니메이션이 조용히 실패합니다.

layout vs layoutId

  • layout — 요소 자체의 크기/위치 변화를 제자리에서 애니메이션합니다.
  • layoutId — 두 개의 개별 요소를 연결하여 렌더링 간에 크로스페이드합니다.

확장되는 컨테이너 내부의 텍스트에 layout="position"을 사용하여 텍스트 재배치가 애니메이션되는 것을 방지하세요.

코드 예제

버튼 피드백

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.button
  whileHover={{ scale: motionTokens.scale.pop }}
  whileTap={{ scale: motionTokens.scale.press }}
  transition={springs.snappy}
/>

스태거 목록

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

const container = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.08,   // 0.05–0.10 규칙 이내
      delayChildren: 0.1,
    },
  },
}

const item = {
  hidden:  { opacity: 0, y: motionTokens.distance.md },
  visible: { opacity: 1, y: 0, transition: springs.gentle },
}

<motion.ul variants={container} initial="hidden" animate="visible">
  {items.map((i) => (
    <motion.li key={i.id} variants={item} />
  ))}
</motion.ul>

모달

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

// 호출 사이트에서 감싸기:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>

export function Modal({ onClose }: { onClose: () => void }) {
  return (
    <>
      {/* 오버레이 */}
      <motion.div
        className="fixed inset-0 bg-black/50"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        onClick={onClose}
      />

      {/* 패널 — 접근성 요구사항: 포커스 트랩, Escape 닫기,
          스크롤 잠금, role="dialog", aria-modal="true" */}
      <motion.div
        role="dialog"
        aria-modal="true"
        className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
        initial={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        animate={{ opacity: 1, scale: 1, y: 0 }}
        exit={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        transition={springs.gentle}
      />
    </>
  )
}

토스트 스택

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<AnimatePresence mode="sync">
  {toasts.map((t) => (
    <motion.div
      key={t.id}
      layout
      initial={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      animate={{ opacity: 1, x: 0, scale: 1 }}
      exit={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      transition={springs.snappy}
    />
  ))}
</AnimatePresence>

페이지 전환 (Next.js App Router)

// components/page-transition.tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { usePathname } from "next/navigation"
import { motionTokens } from "@/lib/motion-tokens"

const variants = {
  initial: { opacity: 0, y: motionTokens.distance.sm },
  enter:   { opacity: 1, y: 0 },
  exit:    { opacity: 0, y: -motionTokens.distance.sm },
}

export function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        variants={variants}
        initial="initial"
        animate="enter"
        exit="exit"
        transition={{
          duration: motionTokens.duration.normal,
          ease: motionTokens.easing.smooth,
        }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

스크롤 시 나타나는 효과

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<motion.div
  initial={{ opacity: 0, y: motionTokens.distance.lg }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: "-80px" }}   // once: true — 규칙 7
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

스크롤 진행률 표시줄

"use client"
import { motion, useScroll } from "motion/react"

export function ScrollProgress() {
  const { scrollYProgress } = useScroll()
  return (
    <motion.div
      className="fixed top-0 left-0 h-1 bg-indigo-500 origin-left w-full"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

확장 카드

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

export function ExpandingCard({ title, body }: { title: string; body: string }) {
  const [expanded, setExpanded] = useState(false)

  return (
    <motion.div layout onClick={() => setExpanded(!expanded)} className="cursor-pointer">
      {/* layout="position"은 텍스트 재배치가 애니메이션되는 것을 방지합니다 */}
      <motion.h2 layout="position" className="font-semibold">
        {title}
      </motion.h2>

      <AnimatePresence>
        {expanded && (
          <motion.p
            key="body"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {body}
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  )
}

공유 요소 크로스페이드

// 소스 컨텍스트
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />

// 대상 컨텍스트 (동일한 layoutId — motion이 전환을 처리합니다)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />

아코디언

<motion.div
  initial={false}
  animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
  style={{ transformOrigin: "top", overflow: "hidden" }}
  transition={{
    duration: motionTokens.duration.normal,
    ease: motionTokens.easing.smooth,
  }}
> {children}
</motion.div>

종단 간 예제

마운트 시 진입하는 스태거 목록으로, 조건부 표시를 처리하고 축소된 모션을 존중하며, 토큰, 스프링, AnimatePresence 및 motion-foundations의 접근성 훅을 결합합니다:

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"

const containerVariants = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08, delayChildren: 0.1 },
  },
}

function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
  const safe = useSafeMotion(motionTokens.distance.sm)
  return (
    <motion.li
      variants={{
        hidden:  safe.initial,
        visible: safe.animate,
      }}
      exit={safe.exit}
      transition={springs.gentle}
      className="flex items-center justify-between p-3 rounded-lg bg-white shadow-sm"
    >
      <span>{label}</span>
      <button onClick={onRemove}>Remove</button>
    </motion.li>
  )
}

export function AnimatedList({ items, onRemove }: {
  items: { id: string; label: string }[]
  onRemove: (id: string) => void
}) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-2"
    >
      <AnimatePresence mode="popLayout">
        {items.map((item) => (
          <ListItem
            key={item.id}
            label={item.label}
            onRemove={() => onRemove(item.id)}
          />
        ))}
      </AnimatePresence>
    </motion.ul>
  )
}

제약 사항 / 비목표

이 스킬은 다음을 다루지 않습니다:

  • 토큰 및 스프링 정의 → motion-foundations 참조
  • 드래그 상호작용, 스와이프 제스처, 재정렬 가능한 목록 → motion-advanced 참조
  • 텍스트 애니메이션 (단어/문자 나타내기, 카운터) → motion-advanced 참조
  • SVG 경로 그리기 또는 모핑 → motion-advanced 참조
  • 사용자 정의 애니메이션 훅 → motion-advanced 참조
  • motion/react를 사용하지 않는 CSS 전용 전환

안티패턴

안티패턴위반된 규칙수정 방법
AnimatePresence 자식에 key 없음규칙 1직접 자식에 안정적인 key 추가
initial + animate에 exit 없음규칙 2항상 세 가지를 함께 정의
mode="wait" 없는 페이지 전환규칙 3AnimatePresence에 mode="wait" 추가
50개 항목 목록에 layout 사용규칙 4mode="popLayout" 또는 명시적 변환 사용
10개 항목 목록에 staggerChildren: 0.2규칙 50.08–0.10으로 제한
포커스 트랩 없는 모달규칙 6focus-trap-react 또는 Radix Dialog 추가
viewport={{ once: true }} 없는 whileInView규칙 7반복 진입은 산만함, 유익하지 않음
인라인 transition={{ duration: 0.3 }}규칙 8motionTokens.duration.normal 사용

관련 스킬

  • motion-foundations — 모든 토큰, 스프링, useSafeMotion 훅 및 이 스킬의 모든 패턴이 가져오는 SSR 가드를 정의합니다. 먼저 설정되어야 합니다.
  • motion-advanced — 드래그, 제스처, SVG, 텍스트, 사용자 정의 훅 및 명령형 시퀀싱으로 이 패턴들을 확장합니다. 이 스킬의 어떤 패턴도 재정의하지 않습니다.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬