motion-foundations
affaan-m/ECC
motion/react를 활용한 React/Next.js용 모션 토큰, 스프링 프리셋, 퍼포먼스 규칙, 디바이스 최적화, 접근성 준수, SSR 안전성. 기초 레이어 — 다른 모든 모션 관련 기술은 이에 기반을 둡니다.
...모든 것을 확장하십시오모션 기초
모션 시스템의 기반 계층입니다. 하위 스킬(motion-patterns, motion-advanced)가 상속받는 모든 값, 제약 조건 및
규칙을 정의합니다.
애니메이션 작업을 시작하기 전에 이 스킬을 불러와야 합니다.
활성화 시점
- 애니메이션 컴포넌트를 처음부터 시작할 때
- 토큰, 스프링 프리셋 또는 이징 값 설정 시
- 구현
prefers-reduced-motion지원 - 애니메이션 초기 상태의 하이드레이션 불일치 디버깅
- 애니메이션이 존재해야 하는지 여부 평가
출력
이 스킬은 다음을 생성합니다:
- 공유
motionTokens객체 (지속 시간, 이징, 거리, 스케일) - 공유
springs프리셋 맵 (이름이 지정된 5개의 구성) - 모든 컴포넌트에서 사용하는
shouldAnimate()모든 컴포넌트에서 사용하는 게이트 - 다음에 따른 접근성 준수 애니메이션 기본값
useReducedMotion - 하이드레이션 경고가 전혀 발생하지 않는 SSR 안전 초기 상태를 통한 접근성 준수 애니메이션 기본값
원칙
모션은 다음 중 적어도 하나를 충족해야 하며, 그렇지 않으면 제거되어야 합니다:
- 주의를 유도해야 함
- 상태 전달
- 공간적 연속성 유지
반응성은 항상 부드러움보다 우선합니다. 입력 지연을 유발하는 60fps 애니메이션은 애니메이션이 없는 것보다 더 나쁩니다.
규칙
이 규칙들은 타협의 여지가 없습니다. 시스템 내 모든 컴포넌트에 적용됩니다.
motion/react만 사용하십시오.framer-motion에서 가져오지 마십시오. 동일한 트리 내에서 두 가지를 혼합해서는 안 됩니다.initial서버 출력과 일치해야 합니다. 서버가opacity: 1,initialprop도opacity: 1이어야 합니다. 예외는 없습니다.- 모션 감소가 모든 규칙보다 우선합니다.
useReducedMotion()가true이거나prefersReduced일때는 모든 변환이 비활성화됩니다. 0.2초 이하에서 불투명도만true인 경우, 모든 변환이 비활성화됩니다. 0.2초 이하의 불투명도만 변경하는 페이드 효과만이 허용되는 대체 방식입니다. - 레이아웃 속성은 절대 애니메이션 처리해서는 안 됩니다.
width,height,top,left,margin,padding다음은animate에서 사용이 금지됩니다.transform와opacity만 사용하십시오. - 모든 토큰 값은
motionTokens에서 가져옵니다. 컴포넌트 파일에 지속 시간이나 이징을 하드코딩하는 것은 금지됩니다. - 모든 스프링 설정은
springs맵에서 가져와야 합니다. 인라인stiffness/damping값은 허용되지 않습니다. "use client"는 다음에서 임포트하는 모든 파일에 필수입니다.motion/react.- 모듈 수준에서
window또는navigator을 절대 읽지 마십시오. 항상 다음으로 보호하십시오.typeof window !== "undefined".
결정 지침
기간 선택
스프링 선택
애니메이션을 완전히 비활성화해야 하는 경우
비활성화(다음으로 설정 shouldAnimate() return false)해야 하는 경우:
prefersReduced다음과 같은 경우trueisLowEnd다음과 같은 경우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
},
}
접근성
우선순위 순서 (높은 순서부터 낮은 순서):
prefers-reduced-motion: reduce— 모든 변환을 비활성화하고, 불투명도 전환 시간을 0.2초 이하로 제한합니다- 저사양 기기 감지 — 지속 시간을 줄이고, 불필요한 애니메이션을 제거합니다
- 디자인 선호도 — 그 외 모든 항목
모션은 점진적으로 성능이 저하되어야 합니다. 레이아웃이 어긋나거나 방향 감각을 혼란스럽게 할 정도로 갑작스럽게 사라져서는 안 됩니다.
// 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시퀀스와 사용자 정의 훅을 추가합니다.
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-motionsupport - Debugging hydration mismatches from animation initial states
- Evaluating whether an animation should exist at all
Outputs
This skill produces:
- A shared
motionTokensobject (duration, easing, distance, scale) - A shared
springspreset 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.
- Use
motion/reactonly. Never import fromframer-motion. Never mix the two in the same tree. initialmust match server output. If the server rendersopacity: 1, theinitialprop must also beopacity: 1. No exceptions.- Reduced motion overrides everything. When
useReducedMotion()returnstrueorprefersReducedistrue, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback. - Never animate layout properties.
width,height,top,left,margin,paddingare banned fromanimate. Usetransformandopacityonly. - All token values come from
motionTokens. Hardcoded durations and easings in component files are forbidden. - All spring configs come from the
springsmap. Inlinestiffness/dampingvalues are forbidden. "use client"is required on every file that imports frommotion/react.- Never read
windowornavigatorat module level. Always guard withtypeof window !== "undefined".
Decision Guidance
Choosing a duration
Choosing a spring
When to disable animation entirely
Disable (make shouldAnimate() return false) when:
prefersReducedistrueisLowEndistrueand 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):
prefers-reduced-motion: reduce— disables all transforms, limits opacity transitions to ≤ 0.2s- Low-end device detection — reduces duration, removes non-essential animations
- 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 withoutmotion/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. AddsuseAnimatesequences and custom hooks on top of this foundation.
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/
복사





집
