Skill v1.0.1
currentAutomated scan100/100+4 new
version: "1.0.1" name: motion-foundations description: 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. Use when setting up motion tokens, spring presets, reduced-motion handling, or SSR-safe animation in React or Next.js. tags: [motion, animation, performance, accessibility] category: frontend author: jeff metadata: version: 1.0.0
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/react` only. Never import from
framer-motion. Never mix the two in the same tree. - `initial` must match server output. If the server renders
opacity: 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 `springs` map. Inline
stiffness/dampingvalues are forbidden. - `"use client"` is required on every file that imports from
motion/react. - Never read `window` or `navigator` at module level. Always guard with
typeof 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
// lib/motion-tokens.tsexport 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.tsexport 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 falseif (!essential && this.isLowEnd()) return falsereturn 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.divinitial={{ 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.ReactNodedelay?: 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 preferredconst safeMotion = useSafeMotion(motionTokens.distance.md)// Device gate — skip animation on low-end hardwareif (!motionConfig.shouldAnimate() || !mounted) {return <div>{children}</div>}return (<motion.divinitial={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. Adds
useAnimatesequences and custom hooks on top of this foundation.