Best fit
- Appropriate Scenarios
- Considerations
- Avoid Using Motion When
affaan-m/ECC
Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.
npx skills add https://github.com/affaan-m/ECC --skill "skills/motion-ui"Source checked Jul 28, 2026·Refresh due Oct 26, 2026
Reorganized from the pinned upstream SKILL.md
According to the pinned SKILL.md from affaan-m/ECC: Production-ready UI motion system for React / Next.js.
npx skills add https://github.com/affaan-m/ECC --skill "skills/motion-ui"Best fit
Bring this context
Expected outputs
Key source sections
Sections are extracted automatically from the pinned SKILL.md and link back to the source.
Use this motion system when motion:
Interactive components (buttons, modals, menus) State transitions (loading → loaded, open → closed) Navigation and layout continuity (shared elements, crossfade)
Accessibility: Always support reduced motion Device adaptation: Adjust for low-end devices Performance trade-offs: Prefer responsiveness over visual smoothness
It is purely decorative It reduces usability or clarity It impacts performance negatively
Guide attention Communicate state Preserve spatial continuity
SkillSignal prompt templates
These prompts were written by SkillSignal from the source structure; they are not upstream text.
Task-start prompt
Confirm source fit, inputs, and outputs before acting.
Use motion-ui to help me with: [specific task]. Context: [files, data, or background]. Constraints: [environment, scope, and prohibited actions]. Before acting, check the pinned SKILL.md and explain which sections apply, what inputs are still missing, and what you will deliver.
Source-guided execution
Make the Agent explicitly follow the key extracted sections.
Apply the pinned motion-ui source to [task]. Pay particular attention to these source sections: “When to Use”, “Appropriate Scenarios”, “Considerations”, “Avoid Using Motion When”, “How It Works”. Preserve the important decision at each step. Mark facts not covered by the source as “needs confirmation” instead of inventing them. Then verify the result against my acceptance criteria: [criteria].
Result-review prompt
Check omissions, permissions, and source drift before delivery.
Review the current motion-ui result: (1) does it satisfy the original task; (2) were any applicable steps or limits in the pinned SKILL.md missed; (3) did it perform any unauthorized file, command, network, or data action; and (4) which conclusions remain unverified? List issues first, then fix only what the source or user authorization supports.
Output checklist
The task matches the purpose documented in the SKILL.md.
The source section “When to Use” has been checked.
The source section “Appropriate Scenarios” has been checked.
The source section “Considerations” has been checked.
The source section “Avoid Using Motion When” has been checked.
Inputs, constraints, and acceptance criteria are explicit.
Unverified facts, compatibility, and outcome claims are clearly marked.
Any file, command, network, or data action has been reviewed.
Choose a different workflow
日本語翻訳:このファイルは motion-ui 用の日本語翻訳が必要です
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detailProduction-ready animation patterns for React / Next.js — button, modal, toast, stagger, page transitions, exit animations, scroll, and layout — built on motion-foundations tokens and springs.
A separate implementation from affaan-m/ECC; compare its source, maintenance signals, and permission requirements.
Open source detailGuide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view
A separate implementation from vercel-labs/agent-skills; compare its source, maintenance signals, and permission requirements.
Open source detailFAQ
Production-ready UI motion system for React / Next.js.
The catalog detected this source-specific install command: npx skills add https://github.com/affaan-m/ECC --skill "skills/motion-ui". Inspect the command and pinned source before running it.
No dedicated Agent platform is declared in the pinned source record.
Quality breakdown
Based on traceable docs and repository signals; stars are not treated as quality.
Compare before choosing
These links are selected from shared tasks, functions, stacks, platforms, and same-name variants. Compare the source owner, documentation, permissions, and maintenance signals.
日本語翻訳:このファイルは motion-ui 用の日本語翻訳が必要です
Production-ready animation patterns for React / Next.js — button, modal, toast, stagger, page transitions, exit animations, scroll, and layout — built on motion-foundations tokens and springs.
Guide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view
Find the bugs a diff can actually ship: TypeScript, React, Next.js, OWASP, accessibility, SEO, database, and deploy-risk review. Return findings, not a grade.
Use this skill whenever the user wants to build scroll animations, scroll effects, parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations, or any motion tied to scroll position — in vanilla JS, React, or Next.js. Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, ScrollSmoother, matchMedia) and Framer Motion / Motion v12 (useScroll, useTransform, useSpring, whileInView, variants). Use this skill even if the user just says "anim
Production-ready UI motion system for React / Next.js.
Focused on performance, accessibility, and usability — not decoration.
Use this motion system when motion:
Motion must:
If it does none → remove it.
npm install motion
motion/react - default for current Motion for React projects (package: motion)framer-motion - legacy import path for projects that still depend on Framer MotionDo not mix. Mixing causes conflicting internal schedulers and broken AnimatePresence contexts — components from one package will not coordinate exit animations with components from the other.
To check which version your project uses:
cat package.json | grep -E '"motion"|"framer-motion"'
Always import from one source consistently:
// Correct (modern)
import { motion, AnimatePresence } from "motion/react"
// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"
// Never mix both in the same project
// motionTokens.ts
export const motionTokens = {
duration: {
fast: 0.18,
normal: 0.35,
slow: 0.6
},
// Use these as the `ease` value inside a `transition` object:
// transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
easing: {
smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
sharp: [0.4, 0, 0.2, 1] as [number, number, number, number]
},
distance: {
sm: 8,
md: 16,
lg: 24
}
}
Usage example:
import { motionTokens } from "@/lib/motionTokens"
<motion.div
initial={{ opacity: 0, y: motionTokens.distance.md }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth
}}
/>
Safe
Avoid
Rule: responsiveness > smoothness
The heuristic combines CPU core count and available memory for a more reliable signal. deviceMemory is available on Chrome/Android; the fallback covers Safari and Firefox.
const isLowEnd =
typeof navigator !== "undefined" && (
// Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
(navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
// Few cores AND no memory API (covers Safari/Firefox on weak hardware)
(navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
)
const duration = isLowEnd ? 0.2 : 0.4
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
/>
)
}
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition {
transition: opacity 0.2s;
}
.motion-reduce-transform {
transform: none !important;
}
}
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
| Scenario | Pattern |
|---|---|
| Hover feedback | whileHover |
| Tap / press feedback | whileTap |
| Reveal on scroll | whileInView |
| Scroll-linked value | useScroll + useTransform |
| Conditional mount/unmount | AnimatePresence |
| Small layout shifts (single element, < ~300px change) | layout prop |
| Large layout shifts or full-page reflows | Avoid layout; use CSS transitions or page-level routing instead |
| Complex, imperative sequences | useAnimate |
Why avoid
layouton large containers? Framer's layout animation usestransformto reconcile positions, but on elements that span the full viewport or trigger deep reflow, the measurement cost causes visible jank and CLS. Prefer CSS Grid/Flexbox transitions or coordinate withlayoutIdon specific child elements only.
layoutId (must be unique per mounted instance)AnimatePresence (see mode guidance below)modeAlways specify mode explicitly — the default ("sync") runs enter and exit simultaneously, which causes visual overlap in most UI patterns.
mode | When to use |
|---|---|
"wait" | Exit completes before enter starts. Use for modals, toasts, page transitions. |
"sync" (default) | Enter and exit overlap. Use only when overlap is intentional (e.g., crossfade carousels). |
"popLayout" | Exiting element is popped out of flow immediately; remaining items animate to fill. Use for lists, tabs, dismissible cards. |
// Modal — always use "wait"
<AnimatePresence mode="wait">
{open && <Modal key="modal" />}
</AnimatePresence>
// Dismissible list item — use "popLayout"
<AnimatePresence mode="popLayout">
{items.map(item => <Card key={item.id} />)}
</AnimatePresence>
layoutId)AnimatePresence mode="wait" so exit animation completes before the next modal entersimport React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"
function useFocusTrap(ref: React.RefObject<HTMLDivElement | null>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return
const el = ref.current
const focusable = el.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
function handleKey(e: KeyboardEvent) {
if (e.key !== "Tab") return
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last?.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
el.addEventListener("keydown", handleKey)
first?.focus()
return () => el.removeEventListener("keydown", handleKey)
}, [active, ref])
}
function useScrollLock(active: boolean) {
useEffect(() => {
if (!active) return
const prev = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => { document.body.style.overflow = prev }
}, [active])
}
function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
const ref = useRef<HTMLDivElement>(null)
useFocusTrap(ref, open)
useScrollLock(open)
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") closeModal()
}
if (open) window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [open, closeModal])
return (
// mode="wait" ensures exit animation finishes before any new modal enters
<AnimatePresence mode="wait">
{open && (
<motion.div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 flex items-center justify-center bg-black/40"
>
<motion.div
ref={ref}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="bg-white p-6 rounded"
>
<h2 id="modal-title">Dialog Title</h2>
<button onClick={closeModal}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
export function Example() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<Modal open={open} closeModal={() => setOpen(false)} />
</>
)
}
initial explicitly)"use client" in Next.js App RouterCheck:
motion/react and framer-motion)"use client" directive in Next.js App Routerkey prop on AnimatePresence childrenlayout prop misuse on large containers causing reflow jankrole="dialog", aria-modal="true")useReducedMotion + CSS media query)AnimatePresence mode set explicitly on all usage siteswidth, height, top, left)staggerChildren ≤ 0.1s; beyond that it feels slow)layout on large or full-viewport containersmode on AnimatePresence (default "sync" causes visual overlap)Motion is interaction design.
If motion does not improve UX → remove it.
import { motion } from "motion/react"
export function Button() {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
>
Click me
</motion.button>
)
}
import { motion, useReducedMotion } from "motion/react"
export function FadeIn() {
const reduce = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: reduce ? 0.1 : 0.35, ease: [0.22, 1, 0.36, 1] }}
/>
)
}
import { motion } from "motion/react"
const container = {
hidden: {},
visible: {
transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
}
}
const item = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}
export function List() {
return (
<motion.ul variants={container} initial="hidden" animate="visible">
{[1, 2, 3].map(i => (
<motion.li key={i} variants={item}>Item {i}</motion.li>
))}
</motion.ul>
)
}
import { motion, AnimatePresence } from "motion/react"
export function Modal({ open }: { open: boolean }) {
return (
<AnimatePresence mode="wait">
{open && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
/>
)}
</AnimatePresence>
)
}
import { useScroll, useTransform, motion } from "motion/react"
export function Parallax() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -80])
return <motion.div style={{ y }} />
}
import { motion } from "motion/react"
export function Skeleton() {
return (
<motion.div
className="bg-gray-200 h-6 w-full rounded"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{
duration: 1.5, // comfortable pulse — was missing, caused fast flash
repeat: Infinity,
ease: "easeInOut"
}}
/>
)
}
import { motion } from "motion/react"
// layoutId must be unique per mounted instance.
// If multiple instances can exist simultaneously, append a unique id:
// layoutId={`shared-${item.id}`}
export function Shared() {
return <motion.div layoutId="shared" />
}