옵션
집집 Skill 데이터 과학 및 ML motion-foundations

motion-foundations

affaan-m/ECC affaan-m/ECC

motion/react를 활용한 React/Next.js용 모션 토큰, 스프링 프리셋, 퍼포먼스 규칙, 디바이스 최적화, 접근성 준수, SSR 안전성. 기초 레이어 — 다른 모든 모션 관련 기술은 이에 기반을 둡니다.

...모든 것을 확장하십시오
51
업데이트 된 시간 2026년 7월 29일

모션 기초

모션 시스템의 기반 계층입니다. 하위 스킬(motion-patterns, motion-advanced)가 상속받는 모든 값, 제약 조건 및 규칙을 정의합니다. 애니메이션 작업을 시작하기 전에 이 스킬을 불러와야 합니다.

활성화 시점

  • 애니메이션 컴포넌트를 처음부터 시작할 때
  • 토큰, 스프링 프리셋 또는 이징 값 설정 시
  • 구현 prefers-reduced-motion 지원
  • 애니메이션 초기 상태의 하이드레이션 불일치 디버깅
  • 애니메이션이 존재해야 하는지 여부 평가

출력

이 스킬은 다음을 생성합니다:

  • 공유 motionTokens 객체 (지속 시간, 이징, 거리, 스케일)
  • 공유 springs 프리셋 맵 (이름이 지정된 5개의 구성)
  • 모든 컴포넌트에서 사용하는 shouldAnimate() 모든 컴포넌트에서 사용하는 게이트
  • 다음에 따른 접근성 준수 애니메이션 기본값 useReducedMotion
  • 하이드레이션 경고가 전혀 발생하지 않는 SSR 안전 초기 상태를 통한 접근성 준수 애니메이션 기본값

원칙

모션은 다음 중 적어도 하나를 충족해야 하며, 그렇지 않으면 제거되어야 합니다:

  • 주의를 유도해야 함
  • 상태 전달
  • 공간적 연속성 유지

반응성은 항상 부드러움보다 우선합니다. 입력 지연을 유발하는 60fps 애니메이션은 애니메이션이 없는 것보다 더 나쁩니다.

규칙

이 규칙들은 타협의 여지가 없습니다. 시스템 내 모든 컴포넌트에 적용됩니다.

  1. motion/react만 사용하십시오. framer-motion에서 가져오지 마십시오. 동일한 트리 내에서 두 가지를 혼합해서는 안 됩니다.
  2. initial 서버 출력과 일치해야 합니다. 서버가 opacity: 1, initial prop도 opacity: 1이어야 합니다. 예외는 없습니다.
  3. 모션 감소가 모든 규칙보다 우선합니다. useReducedMotion() 가 true 이거나 prefersReduced 일때는 모든 변환이 비활성화됩니다. 0.2초 이하에서 불투명도만 true인 경우, 모든 변환이 비활성화됩니다. 0.2초 이하의 불투명도만 변경하는 페이드 효과만이 허용되는 대체 방식입니다.
  4. 레이아웃 속성은 절대 애니메이션 처리해서는 안 됩니다. width, height, top, left, margin, padding 다음은 animate에서 사용이 금지됩니다. transform 와 opacity 만 사용하십시오.
  5. 모든 토큰 값은 motionTokens에서 가져옵니다. 컴포넌트 파일에 지속 시간이나 이징을 하드코딩하는 것은 금지됩니다.
  6. 모든 스프링 설정은 springs 맵에서 가져와야 합니다. 인라인 stiffness/damping 값은 허용되지 않습니다.
  7. "use client" 는 다음에서 임포트하는 모든 파일에 필수입니다. motion/react.
  8. 모듈 수준에서 window 또는 navigator을 절대 읽지 마십시오. 항상 다음으로 보호하십시오. typeof window !== "undefined".

결정 지침

기간 선택

스프링 선택

애니메이션을 완전히 비활성화해야 하는 경우

비활성화(다음으로 설정 shouldAnimate() return false)해야 하는 경우:

  • prefersReduced 다음과 같은 경우 true
  • isLowEnd 다음과 같은 경우 true 이고 애니메이션이 필수적이지 않은 경우
  • 요소가 화면 밖이며 뷰포트에 들어올 일이 전혀 없을 때
  • 애니메이션이 순전히 장식적일 뿐 UX 목적은 없을 때

핵심 개념

토큰 시스템

// lib/motion-tokens.ts
export const motionTokens = {
  duration: {
    instant: 0.08,
    fast:    0.18,
    normal:  0.35,
    slow:    0.6,
    crawl:   1.0,
  },
  easing: {
    smooth: [0.22, 1, 0.36, 1],
    sharp:  [0.4, 0, 0.2, 1],
    bounce: [0.34, 1.56, 0.64, 1],
    linear: [0, 0, 1, 1],
  },
  distance: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 48,
  },
  scale: {
    subtle: 0.98,
    press:  0.95,
    pop:    1.04,
  },
}

export const springs = {
  snappy:  { type: "spring", stiffness: 300, damping: 30 },
  gentle:  { type: "spring", stiffness: 120, damping: 14 },
  bouncy:  { type: "spring", stiffness: 400, damping: 10 },
  instant: { type: "spring", stiffness: 600, damping: 35 },
  release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}

런타임 플래그

// lib/motion-config.ts
export const motionConfig = {
  isLowEnd() {
    return (
      typeof navigator !== "undefined" &&
      navigator.hardwareConcurrency <= 4
    )
  },

  prefersReduced() {
    return (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    )
  },

  shouldAnimate({ essential = false } = {}) {
    if (this.prefersReduced()) return false
    if (!essential && this.isLowEnd()) return false
    return true
  },

  duration() {
    return this.isLowEnd() || this.prefersReduced()
      ? motionTokens.duration.instant
      : motionTokens.duration.normal
  },
}

접근성

우선순위 순서 (높은 순서부터 낮은 순서):

  1. prefers-reduced-motion: reduce — 모든 변환을 비활성화하고, 불투명도 전환 시간을 0.2초 이하로 제한합니다
  2. 저사양 기기 감지 — 지속 시간을 줄이고, 불필요한 애니메이션을 제거합니다
  3. 디자인 선호도 — 그 외 모든 항목

모션은 점진적으로 성능이 저하되어야 합니다. 레이아웃이 어긋나거나 방향 감각을 혼란스럽게 할 정도로 갑작스럽게 사라져서는 안 됩니다.

// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"

export function useSafeMotion(fullY: number = 16) {
  const reduce = useReducedMotion()
  return {
    initial: { opacity: 0, y: reduce ? 0 : fullY },
    animate: { opacity: 1, y: 0 },
    exit:    { opacity: 0, y: reduce ? 0 : -fullY },
  }
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition  { transition: opacity 0.15s; }
  .motion-reduce-transform { transform: none !important; }
}

<div class="motion-safe:animate-fade motion-reduce:opacity-100">div>

SSR / 하이드레이션 안전성

규칙:initial는 항상 서버가 렌더링한 내용과 일치해야 합니다.

// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

div
  initial={{ opacity: mounted ? 0 : 1 }}
  animate={{ opacity: 1 }}
/>

코드 예시

엔드투엔드: 토큰 + 스프링 + 접근성 + SSR 가드

// components/fade-in-card.tsx
"use client"

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

interface FadeInCardProps {
  children: React.ReactNode
  delay?: number
}

export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
  // SSR guard — initial must match server output (opacity: 1)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // Accessibility — disables transform when reduced motion is preferred
  const safeMotion = useSafeMotion(motionTokens.distance.md)

  // Device gate — skip animation on low-end hardware
  if (!motionConfig.shouldAnimate() || !mounted) {
    return <div>{children}div>
  }

  return (
    <motion.div
      initial={safeMotion.initial}
      animate={safeMotion.animate}
      exit={safeMotion.exit}
      transition={{
        ...springs.gentle,
        delay,
      }}
      whileHover={{ scale: motionTokens.scale.pop }}
      whileTap={{ scale: motionTokens.scale.press }}
    >
      {children}
    motion.div>
  )
}

제약 사항 / 목표 외 사항

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

  • UI 컴포넌트 패턴(버튼, 모달, 스태거) → 참조 motion-patterns
  • 드래그, 제스처, SVG, 텍스트 애니메이션, 사용자 정의 훅 → 참조 motion-advanced
  • CSS 전용 애니메이션 또는 Tailwind animate-* 클래스를 사용하지 않는 motion/react
  • 제3자 애니메이션 라이브러리(GSAP, anime.js 등)를 사용하지 않는 CSS 전용 애니메이션
  • 모션 디자인 결정(언제 애니메이션을 적용할지, 무엇을 강조할지) — 이는 코딩상의 제약이 아닌 디자인상의 고려 사항입니다

안티패턴

관련 기술

  • motion-patterns — 여기에서 정의된 토큰과 스프링을 사용하여 버튼, 모달, 스태거, 페이지 전환 및 스크롤 패턴을 구축합니다. 어떤 값도 재정의하지 않습니다.
  • motion-advanced — 드래그, SVG, 텍스트 및 제스처 패턴을 위해 여기에 정의된 토큰과 스프링을 사용합니다. useAnimate 시퀀스와 사용자 정의 훅을 추가합니다.
GitHub에서 보기

Motion Foundations

The base layer of the motion system. Defines every value, constraint, and rule that downstream skills (motion-patterns, motion-advanced) inherit. Load this skill before any animation work begins.

When to Activate

  • Starting any animated component from scratch
  • Setting up tokens, spring presets, or easing values
  • Implementing prefers-reduced-motion support
  • Debugging hydration mismatches from animation initial states
  • Evaluating whether an animation should exist at all

Outputs

This skill produces:

  • A shared motionTokens object (duration, easing, distance, scale)
  • A shared springs preset map (5 named configs)
  • A shouldAnimate() gate used by all components
  • Accessibility-compliant animation defaults via useReducedMotion
  • SSR-safe initial states with zero hydration warnings

Principles

Motion must do at least one of the following or it must be removed:

  • Guide attention
  • Communicate state
  • Preserve spatial continuity

Responsiveness always outranks smoothness. A 60 fps animation that causes input delay is worse than no animation.

Rules

These are non-negotiable. They apply to every component in the system.

  1. Use motion/react only. Never import from framer-motion. Never mix the two in the same tree.
  2. initial must match server output. If the server renders opacity: 1, the initial prop must also be opacity: 1. No exceptions.
  3. Reduced motion overrides everything. When useReducedMotion() returns true or prefersReduced is true, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
  4. Never animate layout properties. width, height, top, left, margin, padding are banned from animate. Use transform and opacity only.
  5. All token values come from motionTokens. Hardcoded durations and easings in component files are forbidden.
  6. All spring configs come from the springs map. Inline stiffness/damping values are forbidden.
  7. "use client" is required on every file that imports from motion/react.
  8. Never read window or navigator at module level. Always guard with typeof window !== "undefined".

Decision Guidance

Choosing a duration

Choosing a spring

When to disable animation entirely

Disable (make shouldAnimate() return false) when:

  • prefersReduced is true
  • isLowEnd is true and the animation is non-essential
  • The element is off-screen and will never enter the viewport
  • The animation is purely decorative with no UX purpose

Core Concepts

Token system

// lib/motion-tokens.ts
export const motionTokens = {
  duration: {
    instant: 0.08,
    fast:    0.18,
    normal:  0.35,
    slow:    0.6,
    crawl:   1.0,
  },
  easing: {
    smooth: [0.22, 1, 0.36, 1],
    sharp:  [0.4, 0, 0.2, 1],
    bounce: [0.34, 1.56, 0.64, 1],
    linear: [0, 0, 1, 1],
  },
  distance: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 48,
  },
  scale: {
    subtle: 0.98,
    press:  0.95,
    pop:    1.04,
  },
}

export const springs = {
  snappy:  { type: "spring", stiffness: 300, damping: 30 },
  gentle:  { type: "spring", stiffness: 120, damping: 14 },
  bouncy:  { type: "spring", stiffness: 400, damping: 10 },
  instant: { type: "spring", stiffness: 600, damping: 35 },
  release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}

Runtime flags

// lib/motion-config.ts
export const motionConfig = {
  isLowEnd() {
    return (
      typeof navigator !== "undefined" &&
      navigator.hardwareConcurrency <= 4
    )
  },

  prefersReduced() {
    return (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    )
  },

  shouldAnimate({ essential = false } = {}) {
    if (this.prefersReduced()) return false
    if (!essential && this.isLowEnd()) return false
    return true
  },

  duration() {
    return this.isLowEnd() || this.prefersReduced()
      ? motionTokens.duration.instant
      : motionTokens.duration.normal
  },
}

Accessibility

Priority order (highest to lowest):

  1. prefers-reduced-motion: reduce — disables all transforms, limits opacity transitions to ≤ 0.2s
  2. Low-end device detection — reduces duration, removes non-essential animations
  3. Design preference — everything else

Motion must degrade gracefully. It must never disappear abruptly in a way that causes layout shift or confuses orientation.

// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"

export function useSafeMotion(fullY: number = 16) {
  const reduce = useReducedMotion()
  return {
    initial: { opacity: 0, y: reduce ? 0 : fullY },
    animate: { opacity: 1, y: 0 },
    exit:    { opacity: 0, y: reduce ? 0 : -fullY },
  }
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition  { transition: opacity 0.15s; }
  .motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>

SSR / hydration safety

Rule: initial must always match what the server renders.

// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

<motion.div
  initial={{ opacity: mounted ? 0 : 1 }}
  animate={{ opacity: 1 }}
/>

Code Examples

End-to-end: tokens + springs + accessibility + SSR guard

// components/fade-in-card.tsx
"use client"

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

interface FadeInCardProps {
  children: React.ReactNode
  delay?: number
}

export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
  // SSR guard — initial must match server output (opacity: 1)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // Accessibility — disables transform when reduced motion is preferred
  const safeMotion = useSafeMotion(motionTokens.distance.md)

  // Device gate — skip animation on low-end hardware
  if (!motionConfig.shouldAnimate() || !mounted) {
    return <div>{children}</div>
  }

  return (
    <motion.div
      initial={safeMotion.initial}
      animate={safeMotion.animate}
      exit={safeMotion.exit}
      transition={{
        ...springs.gentle,
        delay,
      }}
      whileHover={{ scale: motionTokens.scale.pop }}
      whileTap={{ scale: motionTokens.scale.press }}
    >
      {children}
    </motion.div>
  )
}

Constraints / Non-Goals

This skill does not cover:

  • UI component patterns (button, modal, stagger) → see motion-patterns
  • Drag, gestures, SVG, text animations, custom hooks → see motion-advanced
  • CSS-only animations or Tailwind animate-* classes without motion/react
  • Third-party animation libraries (GSAP, anime.js, etc.)
  • Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint

Anti-Patterns

Related Skills

  • motion-patterns — consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.
  • motion-advanced — consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. Adds useAnimate sequences and custom hooks on top of this foundation.

모든 파일

1개 파일

motion-foundations 설치

스킬 파일을 다운로드하여 .claude/skills/ 디렉터리에 압축을 풀어주세요.

ZIP 다운로드

저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.

git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-foundations # Copy the skill folder to .claude/skills/ or .codex/skills/

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

관련 스킬

web-search
업데이트 된 시간 2026년 6월 29일
computer-use
업데이트 된 시간 2026년 7월 29일
webapp-testing
업데이트 된 시간 2026년 6월 29일
lark-base
업데이트 된 시간 2026년 7월 5일
OR