Motion for React skill for Codex

Exit animations that actually play. Codex writes React animation with Motion the way it is meant to be used: state-driven props, AnimatePresence set up so exits really run, layout animations that keep text from stretching, and a shared MotionConfig that respects reduced motion. It knows the motion/react import path, the react-client variant for server components, and when LazyMotion saves real bytes.

  • Frontend
  • $motion-animations
  • 216 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- motion-animations

Installs to ~/.agents/skills/motion-animations/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $motion-animations, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Implements React animation with Motion (the library formerly called Framer Motion, imported from motion/react) - enter and exit with AnimatePresence, layout and shared-element transitions with layoutId, variants and stagger, gestures and drag, scroll-linked values, reduced motion and bundle trimming with LazyMotion. Use when a React or Next.js app needs exit animations, list reorder animations, a shared element morph, swipe-to-dismiss, scroll-linked effects, or when migrating imports from framer-motion.

Use it when

  • A React component needs enter, exit or reorder animation
  • You want a shared-element transition between a list item and a detail view
  • Drag, swipe or scroll-linked effects are needed in React
  • A codebase still imports from framer-motion

Use something else when

How to check the result

Toggle every conditional and confirm exits play, emulate prefers-reduced-motion to confirm transforms stop, and check the bundle analyzer for routes that should not include Motion.

The mistake it prevents

Wrapping AnimatePresence itself in the conditional, or using array indexes as keys, so the exiting element unmounts before its exit animation starts.

~/.agents/skills/motion-animations/SKILL.md

--- name: motion-animations description: Implements React animation with Motion (the library formerly called Framer Motion, imported from motion/react) - enter and exit with AnimatePresence,… license: MIT ---

Done well means: components animate from state, not imperative timing hacks; exits actually play; layout animations do not distort text; reduced-motion users get a calm version; and the animation code does not ship to routes that never use it.

Before writing anything

  1. Read AGENTS.md and package.json. Note whether the project has framer-motion or motion, the React version, and whether it uses the Next.js App Router (server components).
  2. Grep for existing patterns: rg "from \"(framer-motion|motion/react)\"" -n and any shared transition presets. Reuse them.
  3. Installing or upgrading needs network: npm install motion (or the project’s package manager). In a Codex sandbox without network, ask for approval first.

Migrating from framer-motion

The API is the same; the package and import path changed.

npm uninstall framer-motion && npm install motion
rg -l "from ['\"]framer-motion['\"]" src | xargs sed -i '' "s#from ['\"]framer-motion['\"]#from \"motion/react\"#"

(sed -i '' is BSD/macOS; drop the '' on GNU.) Run the type checker afterwards; a few deprecated props from very old Framer Motion versions may surface.

Server components

motion.div uses hooks and must run on the client. In the Next.js App Router:

  • Put animated UI in a file with "use client", or
  • For a simple animated wrapper inside a server component, import import * as motion from "motion/react-client" and use <motion.div> there.

Never add "use client" to a layout or page just to animate one child; push the boundary down.

Pattern table

GoalTool
Fade/slide in on mountinitial + animate
Animate out before removalAnimatePresence + exit
Swap one element for another (step wizard, tab panel)AnimatePresence mode="wait" + changing key
Reorder lists, size changeslayout prop
Thumbnail grows into a modalshared layoutId on both elements
Coordinated childrenvariants + delayChildren: stagger(...)
Hover / press feedbackwhileHover, whileTap, whileFocus
Drag, swipe to dismissdrag, dragConstraints, onDragEnd with velocity
Scroll-linked progress or parallaxuseScroll + useTransform (+ useSpring)
Imperative sequenceuseAnimate

Enter and exit

"use client";
import { AnimatePresence, motion } from "motion/react";

export function Toasts({ items }: { items: Toast[] }) {
  return (
    <ul>
      <AnimatePresence initial={false}>
        {items.map((t) => (
          <motion.li
            key={t.id}
            layout
            initial={{ opacity: 0, y: 12 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, x: 40 }}
            transition={{ type: "spring", stiffness: 500, damping: 40 }}
          >
            {t.message}
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}

Exit rules that cause most “exit does not play” bugs:

  • AnimatePresence must stay mounted; the conditional goes inside it, not around it.
  • Each direct child needs a stable, unique key (never the array index for reorderable lists).
  • The element with exit must be a direct child, or a custom component that renders a motion element and forwards its props.
  • mode="wait" plays one exit before the next enter; mode="popLayout" removes the exiting element from layout flow immediately (pair it with layout on siblings, and the child must forward a ref).

Layout animations

<motion.div layout transition={{ layout: { duration: 0.25 } }}>
  <motion.h3 layout="position">{title}</motion.h3>
  {expanded && <p>{body}</p>}
</motion.div>
  • Layout animation uses transforms, so children can look stretched. Put layout on children too, or layout="position" on text so only position animates.
  • borderRadius and boxShadow are corrected automatically only when set via style or animated by Motion, not via a CSS class.
  • Components that re-layout together but are not siblings need <LayoutGroup>.
  • Scrollable ancestors need layoutScroll; position: fixed ancestors need layoutRoot.

Shared element:

{selected ? (
  <motion.div layoutId={`card-${selected.id}`} className="modal">...</motion.div>
) : (
  items.map((i) => <motion.div key={i.id} layoutId={`card-${i.id}`} onClick={() => select(i)} />)
)}

Wrap the swapping region in AnimatePresence when the old element should also fade.

Variants and stagger

import { motion, stagger } from "motion/react";

const list = { hidden: {}, show: { transition: { delayChildren: stagger(0.05) } } };
const item = { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0 } };

<motion.ul variants={list} initial="hidden" animate="show">
  {rows.map((r) => <motion.li key={r.id} variants={item}>{r.label}</motion.li>)}
</motion.ul>

Children inherit the variant label from the parent; do not set animate on them. Older code uses staggerChildren: 0.05, which still works.

Gestures and drag

<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  dragElastic={0.2}
  onDragEnd={(_, info) => {
    if (info.offset.x > 120 || info.velocity.x > 600) onDismiss();
  }}
  whileTap={{ scale: 0.98 }}
/>
  • Drag must have a keyboard or button alternative (WCAG 2.5.7 Dragging Movements).
  • whileHover does not fire on touch; never hide essential actions behind hover.

Scroll-linked values

const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] });
const y = useTransform(scrollYProgress, [0, 1], [40, -40]);
const smooth = useSpring(scrollYProgress, { stiffness: 120, damping: 30 });
return <motion.section ref={ref} style={{ y }} />;

Motion values update outside React render; do not mirror them into useState on every frame. Use useMotionValueEvent when something must react to a threshold.

Reduced motion and bundle size

import { LazyMotion, domAnimation, MotionConfig } from "motion/react";
import * as m from "motion/react-m";

export function Providers({ children }) {
  return (
    <MotionConfig reducedMotion="user">
      <LazyMotion features={domAnimation} strict>{children}</LazyMotion>
    </MotionConfig>
  );
}
// then use <m.div> instead of <motion.div>
  • reducedMotion="user" disables transform and layout animations for users with the OS setting while keeping opacity and color. Use useReducedMotion() for custom branches (e.g. skip parallax).
  • LazyMotion with m components cuts the initial bundle; use domMax instead of domAnimation only when you need layout animations or drag. strict throws if a full motion component sneaks in.

Gotchas

  • Animating height: "auto" works, but combining it with layout on the same element fights; pick one.
  • initial={false} on AnimatePresence skips enter animations on first paint, which avoids hydration flashes for server-rendered lists.
  • Changing key remounts the subtree and resets its state; use it deliberately.
  • A spring defined by stiffness/damping overrides duration; to think in time, use visualDuration + bounce instead. Do not mix the two styles in one transition.
  • In tests (Jest/Vitest + jsdom) animations do not run; assert on the end state, or set MotionGlobalConfig.skipAnimations = true in setup.

Verification

  • Type check and build pass; no framer-motion imports remain if migrating.
  • Every exit plays: toggle each conditional and watch in the browser at slow speed (DevTools Animations panel does not show JS animations; use the Performance panel).
  • Emulate prefers-reduced-motion: reduce and confirm transforms and layout motion stop.
  • Check bundle impact with the project’s analyzer; routes without motion do not import it.
  • Drag and hover interactions have keyboard equivalents.

Output

Report: components changed, which pattern each uses (presence, layout, layoutId, variants, gesture, scroll), reduced-motion behavior, and bundle effect if LazyMotion was introduced.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Motion for React"
  short_description: "Exit animations that actually play"
  default_prompt: "$motion-animations My AnimatePresence exit animation never runs, fix it"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem