Install
$ agentstack add skill-affaan-m-ecc-motion-patterns ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Motion Patterns
Copy-paste patterns for the most common UI animation needs. Every pattern here is built on motion-foundations tokens and springs. Do not define new duration or easing values here — import them.
When to Activate
- Animating a button, card, modal, or toast notification
- Building list entrances with stagger
- Setting up page transitions in Next.js App Router
- Adding entrance or exit animations to conditional content
- Implementing scroll-reveal, scroll-linked progress, or sticky story sections
- Building expanding cards, accordions, or shared-element transitions
Outputs
This skill produces:
- Accessible, SSR-safe animation for all standard UI components
AnimatePresence-wrapped conditional renders with correct exit behavior- Page transition wrapper component for Next.js App Router
- Scroll-reveal and scroll-linked patterns using
useScroll+useTransform - Layout animation patterns (
layout,layoutId) for expanding and crossfading elements
Principles
- Every pattern imports from
motion-foundations. No raw numbers. - Every conditional render is wrapped in
AnimatePresencewith akey. - Exit animations are always defined alongside enter animations — never as an afterthought.
layoutis used only for small, isolated shifts. Large subtrees get explicit transforms.
Rules
- Always wrap conditional renders in
AnimatePresencewith akeyon the direct child. Without a key, exit animations never fire. - Always define
exitwhen defininginitial+animate. An animation without an exit is incomplete. - Use
mode="wait"on page transitions. Enter must not start until exit completes. - Never use
layouton subtrees with more than ~5 children or deeply nested DOM. Use explicitx/ytransforms instead. - Stagger interval must stay between
0.05sand0.10s. Below feels mechanical; above feels sluggish. - Modals must always include: focus trap, Escape-key close, scroll lock,
role="dialog",aria-modal="true". - Scroll reveals use
viewport={{ once: true }}. Repeating on scroll-out is distracting, not informative. - All token values are imported from
motion-foundations. No inline numbers.
Decision Guidance
Choosing the right pattern
| Situation | Pattern | | ---------------------------------------- | ---------------------- | | Element appears / disappears | AnimatePresence | | List of items loading in sequence | Stagger variants | | Navigating between routes | Page transition wrapper| | Element changes size in place | layout prop | | Same element moves across page contexts | layoutId | | Element enters when scrolled into view | whileInView | | Value tied to scroll position | useScroll + useTransform |
When to use mode="wait" vs mode="sync"
| Mode | Use when | | ------- | --------------------------------------- | | wait | Page transitions, content swaps (one at a time) | | sync | Stacked notifications, list items (overlap is fine) | | popLayout | Items removed from a reflow list |
Core Concepts
AnimatePresence contract
Three things must always be true:
AnimatePresencewraps the conditional- The direct child has a
key - The child has an
exitprop
Miss any one of these and the exit animation silently fails.
layout vs layoutId
layout— animates the element's own size/position change in placelayoutId— links two separate elements, crossfading between them across renders
Use layout="position" on text inside an expanding container to prevent text reflow from animating.
Code Examples
Button feedback
"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"
Stagger list
"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
const container = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.08, // within the 0.05–0.10 rule
delayChildren: 0.1,
},
},
}
const item = {
hidden: { opacity: 0, y: motionTokens.distance.md },
visible: { opacity: 1, y: 0, transition: springs.gentle },
}
{items.map((i) => (
))}
Modal
"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
// Wrap at the call site:
// {isOpen && }
export function Modal({ onClose }: { onClose: () => void }) {
return (
<>
{/* Overlay */}
{/* Panel — accessibility requirements: focus trap, Escape close,
scroll lock, role="dialog", aria-modal="true" */}
)
}
Toast stack
"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
{toasts.map((t) => (
))}
Page transition (Next.js App Router)
// components/page-transition.tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { usePathname } from "next/navigation"
import { motionTokens } from "@/lib/motion-tokens"
const variants = {
initial: { opacity: 0, y: motionTokens.distance.sm },
enter: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -motionTokens.distance.sm },
}
export function PageTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
{children}
)
}
Scroll reveal
"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
Scroll progress bar
"use client"
import { motion, useScroll } from "motion/react"
export function ScrollProgress() {
const { scrollYProgress } = useScroll()
return (
)
}
Expanding card
"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"
export function ExpandingCard({ title, body }: { title: string; body: string }) {
const [expanded, setExpanded] = useState(false)
return (
setExpanded(!expanded)} className="cursor-pointer">
{/* layout="position" prevents text reflow from animating */}
{title}
{expanded && (
{body}
)}
)
}
Shared-element crossfade
// Source context
// Destination context (same layoutId — motion handles the transition)
Accordion
{children}
End-to-End Example
A staggered list that enters on mount, handles conditional presence, and respects reduced motion — combining tokens, springs, AnimatePresence, and the accessibility hook from motion-foundations:
"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
const containerVariants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
}
function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
const safe = useSafeMotion(motionTokens.distance.sm)
return (
{label}
Remove
)
}
export function AnimatedList({ items, onRemove }: {
items: { id: string; label: string }[]
onRemove: (id: string) => void
}) {
return (
{items.map((item) => (
onRemove(item.id)}
/>
))}
)
}
Constraints / Non-Goals
This skill does not cover:
- Token and spring definitions → see
motion-foundations - Drag interactions, swipe gestures, reorderable lists → see
motion-advanced - Text animations (word/character reveal, counters) → see
motion-advanced - SVG path drawing or morphing → see
motion-advanced - Custom animation hooks → see
motion-advanced - CSS-only transitions not using
motion/react
Anti-Patterns
| Anti-pattern | Rule violated | Fix | | -------------------------------------------- | ------- | ------------------------------------------ | | AnimatePresence child missing key | Rule 1 | Add stable key to the direct child | | initial + animate without exit | Rule 2 | Always define all three together | | Page transition without mode="wait" | Rule 3 | Add mode="wait" to AnimatePresence | | layout on a 50-item list | Rule 4 | Use mode="popLayout" or explicit transforms | | staggerChildren: 0.2 on a 10-item list | Rule 5 | Cap at 0.08–0.10 | | Modal without focus trap | Rule 6 | Add focus-trap-react or Radix Dialog | | whileInView without viewport={{ once: true }} | Rule 7 | Repeating entrances distract, not inform | | transition={{ duration: 0.3 }} inline | Rule 8 | Use motionTokens.duration.normal |
Related Skills
motion-foundations— defines all tokens, springs, theuseSafeMotionhook, and SSR guards that every pattern here imports. Must be set up first.motion-advanced— extends these patterns with drag, gestures, SVG, text, custom hooks, and imperative sequencing. Does not redefine any patterns from this skill.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: affaan-m
- Source: affaan-m/ECC
- License: MIT
- Homepage: https://ecc.tools
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.