Communitygithub.com

motion-patterns

프로덕션 준비가 완료된 React / Next.js 애니메이션 패턴 — 버튼, 모달, 토스트, 스테거, 페이지 전환, 종료 애니메이션, 스크롤 및 레이아웃. motion-foundations 토큰과 스프링을 기반으로 구축되었습니다. React 또는 Next.js에서 특정 UI 요소(버튼, 모달, 토스트, 스테거, 페이지 전환 또는 스크롤)에 애니메이션을 적용할 때 사용하세요.

motion-patterns란 무엇인가요?

이 스킬은 React 및 Next.js 애플리케이션에서 자주 사용되는 UI 애니메이션 패턴을 모아놓은 컬렉션입니다. 버튼, 카드, 모달, 토스트 알림, 리스트 스테거, 페이지 전환, 스크롤-리빌, 스크롤 연결 프로그레스, 아코디언 등 다양한 컴포넌트에 대한 복사-붙여넣기 가능한 애니메이션 코드를 제공합니다. 모든 패턴은 `motion-foundations` 라이브러리의 토큰과 스프링을 기반으로 하여 일관된 동작과 접근성을 보장합니다. 특히 `AnimatePresence`를 활용한 조건부 렌더링의 입/퇴장 애니메이션, `mode="wait"`를 사용한 Next.js App Router 페이지 전환, `useScroll`과 `useTransform`을 이용한 스크롤 기반 효과 등을 표준화된 방식으로 구현합니다. 이 스킬은 다양한 애니메이션 시나리오에 대한 의사 결정 가이드, 구체적인 코드 예제, 그리고 자주 발생하는 안티-패턴과 그 해결책을 상세히 제시하여 개발자가 일관되고 접근성 높은 애니메이션을 쉽게 구축할 수 있도록 돕습니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/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에서 가져옵니다. 숫자는 직접 사용하지 않습니다.
  • 모든 조건부 렌더링은 key와 함께 AnimatePresence로 래핑됩니다.
  • 종료 애니메이션은 항상 진입 애니메이션과 함께 정의됩니다. 사후 고려 사항이 아닙니다.
  • layout은 작고 격리된 이동에만 사용됩니다. 큰 하위 트리에는 명시적인 트랜스폼을 사용합니다.

규칙

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

의사 결정 가이드

올바른 패턴 선택하기

상황패턴
요소가 나타나거나 사라짐AnimatePresence
항목 목록이 순서대로 로드됨스테거 variants
라우트 간 이동페이지 전환 래퍼
요소가 제자리에서 크기 변경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>

종단 간 예제

마운트 시 진입하고, 조건부 표시를 처리하며, 움직임 줄이기를 존중하는 스테거 리스트 — motion-foundations의 토큰, 스프링, AnimatePresence 및 접근성 훅을 결합:

"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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

관련 스킬