executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
motionMotion (formerly Framer Motion) is an animation library for React that animates components through declarative props. Use when adding fade-ins, hover and tap effects, exit animations with AnimatePresence, layout and shared-element transitions, scroll-linked or scroll-triggered effects, staggered lists, or when migrating from framer-motion to the motion pack…
Independent GitHub snapshot · Automated classification · Original documentation governs
Motion is the production animation library for React (previously published as framer-motion). Animations are declared as props on motion.* components: initial, animate, exit, whileHover, whileTap, whileInView, layout, layoutId and variants. Springs are the default for physical values, and transform and opacity animations run on the GPU. Version 14.0.0 was published on 2026-10-02; 12.x and 13.x code keeps working for the APIs shown here, but check the changelog if you relied on internal framer-motion APIs.
npm install motion
import { motion, AnimatePresence } from "motion/react";
framer-motion still exists, but new code should use motion and import from motion/react. Migrating means swapping the dependency and changing from "framer-motion" to from "motion/react".motion need "use client" at the top of the file, or import import * as motion from "motion/react-client" so they can render inside server components.motion component is about 34 kB; use LazyMotion with the m component (import * as m from "motion/react-m") and domAnimation (+15 kB: animations, variants, exits, gestures) or domMax (+25 kB: adds drag and layout animations).import { animate, scroll } from "motion" provides the same engine for plain DOM; inside components, useAnimate returns a scope and an animate function for imperative sequences.Wrap the app in <MotionConfig reducedMotion="user"> so transform and layout animations are disabled for people whose OS asks for reduced motion (opacity and colour animations are kept). useReducedMotion() returns the boolean for custom handling, for example setting a parallax y to 0.
import { motion, AnimatePresence } from "motion/react";
// Animate on mount
function FadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }} // Starting state
animate={{ opacity: 1, y: 0 }} // Target state
transition={{ duration: 0.5, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
// Hover and tap interactions
function InteractiveCard({ title }: { title: string }) {
return (
<motion.div
className="card"
whileHover={{ scale: 1.05, boxShadow: "0 10px 30px rgba(0,0,0,0.12)" }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<h3>{title}</h3>
</motion.div>
);
}
// Exit animations
function NotificationList({ notifications }: { notifications: Notification[] }) {
return (
<AnimatePresence>
{notifications.map((n) => (
<motion.div
key={n.id}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100, height: 0 }} // Animate out!
transition={{ type: "spring", damping: 25 }}
>
{n.message}
</motion.div>
))}
</AnimatePresence>
);
}
// Automatic layout animation
function ExpandableCard({ isExpanded, onClick, children }: Props) {
return (
<motion.div
layout // Animate ANY layout change
onClick={onClick}
style={{
width: isExpanded ? 400 : 200,
height: isExpanded ? 300 : 100,
}}
transition={{ layout: { type: "spring", stiffness: 200 } }}
>
<motion.h3 layout="position">{/* Only animate position, not size */}</motion.h3>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
// Shared layout animation (element moves between components)
function TabLayout({ activeTab }: { activeTab: string }) {
return (
<div className="tabs">
{tabs.map((tab) => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="activeTab" // Same layoutId = shared animation
className="underline"
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
)}
</button>
))}
</div>
);
}
import { motion, useScroll, useTransform } from "motion/react";
function ParallaxHero() {
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, -150]);
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
return (
<motion.div style={{ y, opacity }} className="hero">
<h1>Welcome</h1>
</motion.div>
);
}
// Scroll-triggered entrance
function ScrollReveal({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }} // Animate when in viewport
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
>
{children}
</motion.div>
);
}
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
function StaggeredList({ items }: { items: { id: string; name: string }[] }) {
return (
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item}>
{i.name}
</motion.li>
))}
</motion.ul>
);
}
User request: "Make toast notifications slide in from the right and slide out when dismissed, without the list jumping."
Use the exit-animation pattern above and add mode="popLayout" to AnimatePresence so exiting items leave the layout flow, plus layout on each item so siblings glide into the gap:
<AnimatePresence mode="popLayout">
{toasts.map((t) => (
<motion.div key={t.id} layout
initial={{ opacity: 0, x: 100 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 100 }}>
{t.message}
</motion.div>
))}
</AnimatePresence>
Result: new toasts slide in, dismissed ones slide out, remaining toasts move up smoothly. Keys must be stable ids, never array indexes.
User request: "Add a reading-progress bar at the top and fade in each section as it scrolls into view."
import { motion, useScroll, useSpring } from "motion/react";
export function ProgressBar() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 120, damping: 30 });
return <motion.div style={{ scaleX, transformOrigin: "0 0" }} className="fixed inset-x-0 top-0 h-1 bg-emerald-500" />;
}
Wrap each section in the ScrollReveal component above (whileInView, viewport={{ once: true }}). Result: a bar that fills as the page scrolls and sections that fade up once.
layout prop: add it to an element and size or position changes animate automatically. Use layout="position" on text children to avoid stretching, and layout on children of a resized parent to fix scale distortion. Scrollable ancestors need layoutScroll; fixed ancestors need layoutRoot. Wrap components that do not re-render together in LayoutGroup.keys. Modes: sync (default), wait (enter after exit finishes), popLayout. initial={false} skips the first-render animation.type: "spring" with stiffness (speed) and damping (bounce); a duration/bounce pair is easier to tune by feel.whileInView for entrance effects (with viewport={{ once: true }}), useScroll plus useTransform for scroll-linked effects. Prefer these over scroll listeners with setState.layoutId on two elements animates between them (tab underlines, card-to-modal); make layoutIds unique per group.whileHover, whileTap, whileDrag, whileFocus; hover effects do not fire on touch.transform and opacity; animating width, height or top forces layout, so use layout instead. Avoid re-rendering the parent per frame; use motion values (useMotionValue, useTransform) which update without renders.