Motion (Framer Motion) — Skill
Name: motion
Purpose: Implement smooth, accessible animations with motion/react in React.
Use this skill whenever adding animations, gestures, or layout transitions.
Applies when: Using motion/react (motion, AnimatePresence, useScroll, etc.).
Do not use when: No animations are needed or the component is strictly server-rendered.
Rules
- Client-only boundary: Motion requires
'use client'; keep client islands small. - Start simple: Begin with opacity/translate before adding spring physics.
- Use the right tool:
AnimatePresencefor enter/exit,layoutfor layout,whileHover/whileTapfor gestures. - Respect reduced motion: Use
useReducedMotion(). - Bundle discipline: Use
LazyMotionfor larger motion surfaces.
Workflow
- Identify the UX purpose (feedback, transition, focus).
- Add the smallest
'use client'boundary. - Implement enter/exit or layout transitions correctly.
- Respect reduced motion preferences.
- Check performance and bundle size.
Checklists
Implementation checklist
-
'use client'only where needed - Reduced motion behavior defined
-
AnimatePresencewraps conditional UI - Layout transitions use
layout
Review checklist
- Animations don’t re-run on every render
- Motion code doesn’t expand the client tree unnecessarily
Minimal examples
Enter/exit
"use client";
import { AnimatePresence, motion } from "motion/react";
export function Toast({
open,
children,
}: {
open: boolean;
children: React.ReactNode;
}) {
return (
<AnimatePresence>
{open ? (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.18 }}
>
{children}
</motion.div>
) : null}
</AnimatePresence>
);
}
Layout animation
"use client";
import { motion } from "motion/react";
export function Expander({ open }: { open: boolean }) {
return (
<motion.div layout className="overflow-hidden rounded-md border p-3">
<div className="font-medium">Title</div>
{open ? <div className="mt-2 text-sm">More content...</div> : null}
</motion.div>
);
}
Reduced motion
"use client";
import { motion, useReducedMotion } from "motion/react";
export function FadeIn({ children }: { children: React.ReactNode }) {
const reduce = useReducedMotion();
return (
<motion.div
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
);
}
Common mistakes / pitfalls
- Unmounting without
AnimatePresence(exit animations never run) - Animating layout without
layout - Putting Motion in a huge client tree
- Animating large lists without virtualization