motion-patterns
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. Use when animating a specific UI element in React or Next.js — button, modal, toast, stagger, page transiti
- 0
- Installs
- —
- Rating
- —
- Success rate
- 1
- Files scanned
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 2956d394f3fa422e… — run codexguild_scan_skills after installing to verify your local copy.
Static analysis is a first line of defense, not a guarantee. Read the source
SKILL.md
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"
<motion.button
whileHover={{ scale: motionTokens.scale.pop }}
whileTap={{ scale: motionTokens.scale.press }}
transition={springs.snappy}
/>
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 },
}
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map((i) => (
<motion.li key={i.id} variants={item} />
))}
</motion.ul>
Modal
"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
// Wrap at the call site:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>
export function Modal({ onClose }: { onClose: () => void }) {
return (
<>
{/* Overlay */}
<motion.div
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* Panel — accessibility requirements: focus trap, Escape close,
scroll lock, role="dialog", aria-modal="true" */}
<motion.div
role="dialog"
aria-modal="true"
className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
initial={{
opacity: 0,
scale: motionTokens.scale.press,
y: motionTokens.distance.sm,
}}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{
opacity: 0,
scale: motionTokens.scale.press,
y: motionTokens.distance.sm,
}}
transition={springs.gentle}
/>
</>
)
}
Toast stack
"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
<AnimatePresence mode="sync">
{toasts.map((t) => (
<motion.div
key={t.id}
layout
initial={{
opacity: 0,
x: motionTokens.distance.xl,
scale: motionTokens.scale.subtle,
}}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{
opacity: 0,
x: motionTokens.distance.xl,
scale: motionTokens.scale.subtle,
}}
transition={springs.snappy}
/>
))}
</AnimatePresence>
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 (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
variants={variants}
initial="initial"
animate="enter"
exit="exit"
transition={{
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth,
}}
>
{children}
</motion.div>
</AnimatePresence>
)
}
Scroll reveal
"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
<motion.div
initial={{ opacity: 0, y: motionTokens.distance.lg }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }} // once: true — rule 7
transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>
Scroll progress bar
"use client"
import { motion, useScroll } from "motion/react"
export function ScrollProgress() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 h-1 bg-indigo-500 origin-left w-full"
style={{ scaleX: scrollYProgress }}
/>
)
}
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 (
<motion.div layout onClick={() => setExpanded(!expanded)} className="cursor-pointer">
{/* layout="position" prevents text reflow from animating */}
<motion.h2 layout="position" className="font-semibold">
{title}
</motion.h2>
<AnimatePresence>
{expanded && (
<motion.p
key="body"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: motionTokens.duration.fast }}
>
{body}
</motion.p>
)}
</AnimatePresence>
</motion.div>
)
}
Shared-element crossfade
// Source context
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />
// Destination context (same layoutId — motion handles the transition)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />
Accordion
<motion.div
initial={false}
animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
style={{ transformOrigin: "top", overflow: "hidden" }}
transition={{
duration: motionTokens.duration.normal,
ease: motionTokens.easing.smooth,
}}
> {children}
</motion.div>
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 (
<motion.li
variants={{
hidden: safe.initial,
visible: safe.animate,
}}
exit={safe.exit}
transition={springs.gentle}
className="flex items-center justify-between p-3 rounded-lg bg-white shadow-sm"
>
<span>{label}</span>
<button onClick={onRemove}>Remove</button>
</motion.li>
)
}
export function AnimatedList({ items, onRemove }: {
items: { id: string; label: string }[]
onRemove: (id: string) => void
}) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-2"
>
<AnimatePresence mode="popLayout">
{items.map((item) => (
<ListItem
key={item.id}
label={item.label}
onRemove={() => onRemove(item.id)}
/>
))}
</AnimatePresence>
</motion.ul>
)
}
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.
Files
1- SKILL.md
9d78bcd2c313.1 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from affaan-m/everything-claude-code8
Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels, accessibility traits and hints, focus management, contrast, target size, and screen-reader support. Use when building or auditing UI for accessibility compliance, keyboard n
Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent ap
Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.
Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent's tool set, action space, or observation format.
Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.
Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk, X Layer through OKX Payments / OKX Agent Payments Protocol, and Solana plus multi-network EVM through the upstream x402 packages with facilitator-based
Security hardening guidance for AI agent frameworks that process untrusted content, invoke tools, write workspace files, manage runtime identifiers, or handle credentials. Use when building or reviewing an agent runtime, autonomous worker, tool gateway, memory service, or multi-tenant agent deployme
Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.
Related frontend skillsscan passed
Hands-off, diff-scoped browser QA of the active branch: maps user flows, drives a real browser, autonomously fixes small breakages with regression tests and commits, judges experience against product personas, and writes a durable dogfood report. Manual invocation only.
PostHog integration for React applications built with Vite (no router)
Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert radix primitives, or switch a shadcn project's base library. Handles single components ("migrate accordion") and whole projects.
Set HTTP cache headers on tRPC query responses via responseMeta callback for CDN and browser caching. Configure Cache-Control, s-maxage, stale-while-revalidate. Handle caching with batching and authenticated requests. Avoid caching mutations, errors, and authenticated responses.
Builds production-quality, accessible, responsive user-facing UIs. Use when building or modifying interfaces and pages, creating components, implementing layouts, meeting WCAG accessibility requirements, managing state, or when the output needs to look and feel production-quality rather than AI-gene
Guides Metronome usage-based billing integration decisions — event ingestion (single and batch, idempotency, billable metrics), contract design (rate cards, overrides, dimensional pricing, products), invoicing lifecycle (grace periods, finalization, Stripe sync), credit and commit management (prepai