All topics
Frontend · Learning hub

Framer-motion notes for developers

Master Framer-motion with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Framer-motion

Framer Motion Animations

Framer Motion Animations Core Concepts import { motion, AnimatePresence, useAnimation, useInView, useScroll, useTransform } from 'framer-motion'; // Basic anima

Framer Motion Animations

Core Concepts

import { motion, AnimatePresence, useAnimation, useInView, useScroll, useTransform } from 'framer-motion';

// Basic animation — motion.div replaces div
<motion.div
  initial={{ opacity: 0, y: 20 }}     // starting state
  animate={{ opacity: 1, y: 0 }}      // target state
  exit={{ opacity: 0, y: -20 }}       // unmount animation (needs AnimatePresence)
  transition={{ duration: 0.3, ease: 'easeOut' }}
/>

// Hover & tap
<motion.button
  whileHover={{ scale: 1.05, boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>

// Variants — define states by name
const cardVariants = {
  hidden: { opacity: 0, y: 30 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4, ease: 'easeOut' },
  },
  hover: { scale: 1.02 },
};

<motion.div
  variants={cardVariants}
  initial="hidden"
  animate="visible"
  whileHover="hover"
/>

// Stagger children
const containerVariants = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.1,  // delay between each child
      delayChildren: 0.2,
    },
  },
};
const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: { opacity: 1, x: 0 },
};

<motion.ul variants={containerVariants} initial="hidden" animate="visible">
  {items.map(item => (
    <motion.li key={item.id} variants={itemVariants}>
      {item.name}
    </motion.li>
  ))}
</motion.ul>

AnimatePresence & Page Transitions

// AnimatePresence — enables exit animations
function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          <motion.div          // backdrop
            key="backdrop"
            className="fixed inset-0 bg-black/50"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
          />
          <motion.div          // modal
            key="modal"
            className="fixed inset-x-0 bottom-0 rounded-t-2xl bg-white p-6"
            initial={{ y: '100%' }}
            animate={{ y: 0 }}
            exit={{ y: '100%' }}
            transition={{ type: 'spring', damping: 30, stiffness: 300 }}
          >
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
}

// Route transitions (Next.js App Router)
// app/template.tsx — re-mounts on each navigation
'use client';
export default function Template({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

// List add/remove with layout animations
<AnimatePresence mode="popLayout">
  {items.map(item => (
    <motion.div
      key={item.id}
      layout                            // animate layout changes
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
    >
      {item.name}
    </motion.div>
  ))}
</AnimatePresence>

Scroll & Gesture Hooks

// Scroll-linked animations
function ParallaxHero() {
  const { scrollYProgress } = useScroll();
  const y = useTransform(scrollYProgress, [0, 1], [0, -300]);
  const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0]);

  return (
    <motion.div style={{ y, opacity }}>
      <h1>Hero Text</h1>
    </motion.div>
  );
}

// Animate when element enters viewport
function AnimateOnScroll({ children }) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: '-100px' });

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 40 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  );
}

// Programmatic control
function PulseButton() {
  const controls = useAnimation();

  const pulse = async () => {
    await controls.start({ scale: 1.2, transition: { duration: 0.2 } });
    await controls.start({ scale: 1, transition: { duration: 0.2 } });
  };

  return <motion.button animate={controls} onClick={pulse}>Click me</motion.button>;
}

// Drag
<motion.div
  drag="x"                        // 'x' | 'y' | true (both)
  dragConstraints={{ left: -100, right: 100 }}
  dragElastic={0.2}
  onDragEnd={(e, info) => console.log(info.offset, info.velocity)}
/>

Keep your Framer-motion knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever