motion-foundations
Motion tokens, spring presets, performance rules, device adaptation, accessibility enforcement, and SSR safety for React / Next.js using motion/react. Foundation layer — all other motion skills depend on this.
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
| Token | Use when |
|---|---|
instant | Tooltip show/hide, focus ring, badge update |
fast | Button feedback, icon swap, chip toggle |
normal | Modal open, card expand, page element enter |
slow | Hero entrance, full-page transition |
crawl | Deliberate storytelling; use sparingly |
Choosing a spring
| Preset | Use when |
|---|---|
snappy | Default UI — buttons, chips, nav items |
gentle | Cards, modals, panels landing softly |
bouncy | Playful moments — empty states, onboarding |
instant | Tooltips, popovers, dropdowns |
release | Drag release — natural physics feel |
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
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
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.
"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 }, }}@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
"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
| Anti-pattern | Rule violated | Fix |
|---|---|---|
import { motion } from "framer-motion" | Rule 1 | Use motion/react |
initial={{ opacity: 0 }} on SSR component | Rule 2 | Add mount guard |
Skipping useReducedMotion check | Rule 3 | Use useSafeMotion hook |
animate={{ width: "100%" }} | Rule 4 | Use scaleX transform instead |
transition={{ duration: 0.4 }} inline | Rule 5 | Use motionTokens.duration.normal |
{ stiffness: 300, damping: 30 } inline | Rule 6 | Use springs.snappy |
Missing "use client" directive | Rule 7 | Add to top of file |
navigator.hardwareConcurrency at module level | Rule 8 | Wrap in typeof navigator !== "undefined" |
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.