motion-foundations
affaan-m/ecc
Foundation layer for React/Next.js animations: tokens, springs, accessibility rules, and SSR safety using motion/react.
What is motion-foundations?
Motion Foundations establishes the base system for all animation work in React and Next.js projects using motion/react. It defines reusable tokens (durations, easings, distances, scales), spring presets, accessibility enforcement via prefers-reduced-motion, device adaptation, and SSR-safe patterns. Load this skill before any other motion skills.
- Provides shared motionTokens object with 5 duration levels, 4 easing curves, 5 distance values, and 3 scale presets
- Exports 5 named spring configurations (snappy, gentle, bouncy, instant, release) for consistent physics-based animation
- Implements shouldAnimate() gate that respects prefers-reduced-motion, low-end device detection, and essential-vs-decorative flags
- Enforces accessibility via useReducedMotion hook and CSS utilities that disable transforms and limit opacity transitions
- Ensures SSR hydration safety by requiring initial state to match server output and providing client-mount guards
How to install motion-foundations
npx skills add null --skill motion-foundations- React 16.8+ or Next.js 13+
- motion/react package installed (not framer-motion)
- TypeScript recommended but not required
How to use motion-foundations
- 1.Create lib/motion-tokens.ts and export motionTokens object with duration, easing, distance, and scale values
- 2.Create lib/motion-config.ts and export motionConfig with isLowEnd(), prefersReduced(), shouldAnimate(), and duration() methods
- 3.Create hooks/use-reduced-motion.tsx and export useSafeMotion hook that returns initial/animate/exit states respecting accessibility
- 4.Add "use client" directive to any component importing from motion/react
- 5.Use motionTokens and springs presets in all animated components instead of hardcoded values; guard window/navigator access with typeof checks
Use cases
- Setting up a new React/Next.js project with animated components and ensuring consistent motion across all features
- Implementing prefers-reduced-motion support so animations gracefully degrade for users with motion sensitivity
- Debugging hydration mismatches caused by animation initial states not matching server-rendered output
- Adapting animations for low-end devices by detecting hardware concurrency and disabling non-essential motion
- Building a design system where all downstream animation skills inherit tokens and rules from this foundation
- Frontend engineers building animated React/Next.js applications
- Design systems teams establishing motion standards and constraints
- Developers implementing accessibility-compliant animations
- Teams requiring SSR-safe animation patterns without hydration warnings
motion-foundations FAQ
motion/react is the new package from Framer; framer-motion is the legacy package. This skill requires motion/react only. Never mix them in the same tree.
If the server renders opacity:1 but initial says opacity:0, React will detect a hydration mismatch and cause console warnings and potential layout shifts. Always ensure initial matches what the server renders, or use a mounted guard.
Disable animation (make shouldAnimate() return false) when prefers-reduced-motion is true, the device is low-end and animation is non-essential, the element is off-screen, or the animation is purely decorative with no UX purpose.
Duration tokens (instant, fast, normal, slow, crawl) are used with tween animations for fixed timing. Spring presets (snappy, gentle, bouncy, instant, release) are used with spring animations for physics-based motion. Choose based on the feel you want.
Use a mounted state guard: set initial to match server output (usually opacity:1, y:0), then use useEffect to set mounted=true and animate from there. Or use AnimatePresence to defer animation until client-side.
Full instructions (SKILL.md)
Source of truth, from affaan-m/ecc.
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. version: 1.0 tags: [motion, animation, performance, accessibility] category: frontend author: jeff
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
// 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
| 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.
Related skills
More from affaan-m/ecc and the wider catalog.
motion-patterns
Production-ready animation patterns for React/Next.js built on motion-foundations tokens.
motion-ui
Production-ready UI motion system for React/Next.js with performance, accessibility, and usability focus.
mysql-patterns
MySQL and MariaDB schema, indexing, transaction, and replication patterns for production backends.
nanoclaw-repl
Session-aware REPL for Claude with persistent markdown storage and dynamic skill loading.
nasiko-control-plane
Agent skill from affaan-m/ecc.
nestjs-patterns
Production-grade NestJS patterns for modular TypeScript backends with validation, guards, and config.