Animation Recipes

Copy-paste animation snippets using Framer Motion for common UI patterns

Animation Recipes

Quick copy-paste animation snippets for common UI patterns.

Basic Animations

Fade In

<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}>
  Content
</motion.div>

Slide Up

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  Content
</motion.div>

Scale In

<motion.div
  initial={{ opacity: 0, scale: 0.8 }}
  animate={{ opacity: 1, scale: 1 }}
  transition={{ duration: 0.3 }}
>
  Content
</motion.div>

Button Animations

Hover Scale

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
  Click me
</motion.button>

Lift on Hover

<motion.button
  whileHover={{ y: -2, boxShadow: "0 4px 12px rgba(0,0,0,0.15)" }}
  whileTap={{ y: 0 }}
  transition={{ duration: 0.2 }}
>
  Hover me
</motion.button>

Ripple Effect

<motion.button
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
  <motion.div
    initial={{ scale: 0, opacity: 1 }}
    animate={{ scale: 1.5, opacity: 0 }}
    transition={{ duration: 0.5 }}
  />
  Tap me
</motion.button>

Card Animations

Hover Card

<motion.div
  className="card"
  whileHover={{
    y: -8,
    boxShadow: "0 20px 40px rgba(0,0,0,0.1)",
  }}
  transition={{
    type: "spring",
    stiffness: 300,
    damping: 20,
  }}
>
  Card content
</motion.div>

Stacked Cards

const cards = [1, 2, 3];
 
<div className="relative">
  {cards.map((card, i) => (
    <motion.div
      key={card}
      className="card absolute"
      initial={{ y: i * 10, scale: 1 - i * 0.05 }}
      whileHover={{ y: i * -20, scale: 1 }}
      transition={{ type: "spring", stiffness: 300, damping: 25 }}
    >
      Card {card}
    </motion.div>
  ))}
</div>;

List Animations

Stagger Children

const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
};
 
const item = {
  hidden: { opacity: 0, x: -20 },
  show: { opacity: 1, x: 0 },
};
 
<motion.ul variants={container} initial="hidden" animate="show">
  {items.map((item) => (
    <motion.li key={item.id} variants={item}>
      {item.text}
    </motion.li>
  ))}
</motion.ul>;

List Item Exit

import { AnimatePresence } from "framer-motion";
 
<AnimatePresence>
  {items.map((item) => (
    <motion.li
      key={item.id}
      initial={{ opacity: 0, height: 0 }}
      animate={{ opacity: 1, height: "auto" }}
      exit={{ opacity: 0, height: 0 }}
      transition={{ duration: 0.3 }}
    >
      {item.text}
    </motion.li>
  ))}
</AnimatePresence>;

Fade + Scale Modal

import { AnimatePresence } from "framer-motion";
 
<AnimatePresence>
  {isOpen && (
    <>
      {/* Backdrop */}
      <motion.div
        className="fixed inset-0 bg-black/50"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        onClick={() => setIsOpen(false)}
      />
 
      {/* Modal */}
      <motion.div
        className="fixed inset-0 flex items-center justify-center"
        initial={{ opacity: 0, scale: 0.9 }}
        animate={{ opacity: 1, scale: 1 }}
        exit={{ opacity: 0, scale: 0.9 }}
        transition={{ type: "spring", stiffness: 300, damping: 25 }}
      >
        <div className="modal-content">Modal content</div>
      </motion.div>
    </>
  )}
</AnimatePresence>;

Slide-In Modal

<motion.div
  initial={{ x: "100%" }}
  animate={{ x: 0 }}
  exit={{ x: "100%" }}
  transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
  Modal content
</motion.div>

Scroll Animations

Reveal on Scroll

import { motion, useInView } from "framer-motion";
import { useRef } from "react";
 
const ref = useRef(null);
const isInView = useInView(ref, { once: true });
 
<motion.div
  ref={ref}
  initial={{ opacity: 0, y: 50 }}
  animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
  transition={{ duration: 0.6 }}
>
  Content reveals when scrolled into view
</motion.div>;

Parallax Effect

import { motion, useScroll, useTransform } from "framer-motion";
 
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, 150]);
 
<motion.div style={{ y }}>Parallax content</motion.div>;

Progress Bar

import { motion, useScroll } from "framer-motion";
 
const { scrollYProgress } = useScroll();
 
<motion.div className="progress-bar" style={{ scaleX: scrollYProgress }} />;
const [isOpen, setIsOpen] = useState(false);
 
<motion.button onClick={() => setIsOpen(!isOpen)} animate={isOpen ? "open" : "closed"}>
  <motion.span
    variants={{
      closed: { rotate: 0, y: 0 },
      open: { rotate: 45, y: 6 },
    }}
  />
  <motion.span
    variants={{
      closed: { opacity: 1 },
      open: { opacity: 0 },
    }}
  />
  <motion.span
    variants={{
      closed: { rotate: 0, y: 0 },
      open: { rotate: -45, y: -6 },
    }}
  />
</motion.button>;
<AnimatePresence>
  {isOpen && (
    <motion.div
      initial={{ opacity: 0, y: -10 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -10 }}
      transition={{ duration: 0.2 }}
    >
      Menu items
    </motion.div>
  )}
</AnimatePresence>

Loading States

Spinner

<motion.div
  animate={{ rotate: 360 }}
  transition={{
    duration: 1,
    repeat: Infinity,
    ease: "linear",
  }}
>
  <LoadingIcon />
</motion.div>

Pulse

<motion.div
  animate={{
    scale: [1, 1.1, 1],
    opacity: [1, 0.8, 1],
  }}
  transition={{
    duration: 2,
    repeat: Infinity,
    ease: "easeInOut",
  }}
>
  Loading...
</motion.div>

Skeleton Shimmer

<motion.div
  className="skeleton"
  animate={{
    backgroundPosition: ["200% 0", "-200% 0"],
  }}
  transition={{
    duration: 2,
    repeat: Infinity,
    ease: "linear",
  }}
  style={{
    background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
    backgroundSize: "200% 100%",
  }}
/>

Advanced Patterns

Magnetic Button

import { motion, useMotionValue, useSpring } from "framer-motion";
 
const x = useMotionValue(0);
const y = useMotionValue(0);
 
const springConfig = { damping: 25, stiffness: 300 };
const springX = useSpring(x, springConfig);
const springY = useSpring(y, springConfig);
 
<motion.button
  style={{ x: springX, y: springY }}
  onMouseMove={(e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;
    x.set((e.clientX - centerX) * 0.3);
    y.set((e.clientY - centerY) * 0.3);
  }}
  onMouseLeave={() => {
    x.set(0);
    y.set(0);
  }}
>
  Magnetic button
</motion.button>;

Smooth Scroll Progress

import { motion, useScroll, useSpring } from "framer-motion";
 
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
  stiffness: 100,
  damping: 30,
  restDelta: 0.001,
});
 
<motion.div
  className="fixed left-0 right-0 top-0 h-1 origin-left bg-blue-500"
  style={{ scaleX }}
/>;

Page Transition

// layout.tsx
import { AnimatePresence } from "framer-motion";
 
<AnimatePresence mode="wait">
  <motion.div
    key={pathname}
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    exit={{ opacity: 0, y: -20 }}
    transition={{ duration: 0.3 }}
  >
    {children}
  </motion.div>
</AnimatePresence>;

Tips for Best Results

  1. Always use transform properties (x, y, scale) instead of layout properties
  2. Add will-change: transform CSS for frequently animated elements
  3. Use spring physics for natural, organic movement
  4. Respect user preferences with useReducedMotion()
  5. Keep animations under 0.5s for UI feedback
  6. Test on mobile devices - animations feel different on touch screens

Performance Checklist

  • Using transform properties (not width/height/top/left)
  • Added will-change: transform CSS
  • Using useMotionValue for frequently updated values
  • Wrapped exit animations in AnimatePresence
  • Tested with reduced motion preferences
  • Verified 60fps on mobile devices

Common Mistakes

Don't animate layout properties:
<motion.div animate={{ width: 200 }} />
Use transform instead:
<motion.div animate={{ scale: 1.5 }} />
Don't forget AnimatePresence for exits:
{
  show && <motion.div exit={{ opacity: 0 }} />;
}
Wrap with AnimatePresence:
<AnimatePresence>{show && <motion.div exit={{ opacity: 0 }} />}</AnimatePresence>
Happy animating! 🎉