32blogby Studio Mitsu

Motion for React Animation Guide (formerly Framer Motion)

From fade-ins to scroll-triggered effects and page transitions — practical Motion (formerly Framer Motion) patterns with working code examples for React.

by omitsu11 min read
ReactFramer MotionanimationCSSUX
On this page

Motion for React (formerly Framer Motion) is a declarative animation library that handles fade-ins, scroll-triggered effects, exit animations, and layout transitions through simple props like initial, animate, and exit — all while staying in sync with React state.

Before this library existed, I spent too much time wrestling with @keyframes and CSS transitions. Interactive animations — hover effects, modals, page transitions — got especially messy when mixed with React state management. Motion changed that entirely. It's declarative, works naturally with React state, and handles the tricky parts (like animating components as they unmount) automatically. This article covers practical patterns from basic fade-ins to scroll-triggered effects and layout animations.

Core Concepts

Motion centers around the motion component. Prefix any HTML element with motion. and it gains animation capabilities:

tsx
import { motion } from "motion/react";

function FadeInBox() {
  return (
    <motion.div
      initial={{ opacity: 0 }}    // Starting state
      animate={{ opacity: 1 }}    // Target state
      transition={{ duration: 0.5 }} // Animation settings
    >
      This fades in on mount
    </motion.div>
  );
}

Three props to learn:

  • initial — state before the animation starts
  • animate — target state to animate to
  • transition — duration, easing, delay, etc.

Common Animation Properties

tsx
<motion.div
  initial={{ opacity: 0, y: 20, scale: 0.95 }}
  animate={{ opacity: 1, y: 0, scale: 1 }}
  transition={{
    duration: 0.4,
    ease: "easeOut",
    delay: 0.1,
  }}
>
  Content
</motion.div>

Almost any CSS property works, but prefer opacity and transform properties (x, y, scale, rotate). They're GPU-accelerated and don't trigger layout reflow — keeping your animations smooth.

Fade and Slide Patterns

Starting with the most common patterns.

Page Load Fade-in

tsx
"use client";
import { motion } from "motion/react";

function HeroSection() {
  return (
    <section>
      <motion.h1
        initial={{ opacity: 0, y: 30 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.6, ease: "easeOut" }}
      >
        Page Title
      </motion.h1>

      <motion.p
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.6, ease: "easeOut", delay: 0.15 }}
      >
        Subtext here
      </motion.p>

      <motion.button
        initial={{ opacity: 0, y: 15 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5, ease: "easeOut", delay: 0.3 }}
      >
        Get Started
      </motion.button>
    </section>
  );
}

Incrementing delay creates a stagger effect — elements appear sequentially.

Hover Animations

tsx
"use client";
import { motion } from "motion/react";

function AnimatedCard({ title, description }: { title: string; description: string }) {
  return (
    <motion.div
      className="rounded-lg border p-6 cursor-pointer"
      whileHover={{
        scale: 1.02,
        boxShadow: "0 10px 40px rgba(0,0,0,0.15)",
      }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 400, damping: 30 }}
    >
      <h3>{title}</h3>
      <p>{description}</p>
    </motion.div>
  );
}

whileHover and whileTap are the declarative equivalents of CSS :hover and :active.

Variants for Complex Animations

When multiple elements animate together, use variants — named animation states that can be shared between parent and children.

tsx
"use client";
import { motion } from "motion/react";

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1, // Stagger children by 0.1s
      delayChildren: 0.2,   // Wait 0.2s before starting children
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4, ease: "easeOut" },
  },
};

function AnimatedList({ items }: { items: string[] }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item, i) => (
        <motion.li key={i} variants={itemVariants}>
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

staggerChildren does the heavy lifting here. When the container transitions to "visible", each child starts its own animation with a delay offset. No manual delay management needed — just pass the same variants to each motion.li.

Scroll-Triggered Animations (whileInView)

whileInView triggers an animation when the element enters the viewport:

tsx
"use client";
import { motion } from "motion/react";

function FeatureCard({ icon, title, description }: FeatureCardProps) {
  return (
    <motion.div
      className="rounded-xl p-8 border"
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-50px" }} // Trigger 50px before fully in view
      transition={{ duration: 0.5, ease: "easeOut" }}
    >
      <div className="text-4xl mb-4">{icon}</div>
      <h3 className="text-xl font-bold mb-2">{title}</h3>
      <p>{description}</p>
    </motion.div>
  );
}

viewport options:

  • once: true — animate only the first time it enters view (recommended for content reveals)
  • margin — offset from the viewport edge; negative values trigger before fully visible

AnimatePresence: Animate Mount and Unmount

By default, React removes components from the DOM instantly. AnimatePresence holds them in the DOM until their exit animation completes.

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

function Modal({ isOpen, onClose, children }: ModalProps) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          <motion.div
            key="overlay"
            className="fixed inset-0 bg-black/50"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
          />

          <motion.div
            key="modal"
            className="fixed inset-x-4 top-1/2 -translate-y-1/2 max-w-lg mx-auto
                       bg-white rounded-2xl p-8 shadow-2xl z-10"
            initial={{ opacity: 0, scale: 0.95, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.95, y: 20 }}
            transition={{ type: "spring", stiffness: 350, damping: 30 }}
          >
            {children}
            <button onClick={onClose}>Close</button>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
}

The exit prop defines the animation that plays before the component is removed. Without AnimatePresence, exit is ignored.

Toast Notifications

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

type Toast = { id: number; message: string };

function ToastContainer() {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string) => {
    const id = Date.now();
    setToasts(prev => [...prev, { id, message }]);
    setTimeout(() => {
      setToasts(prev => prev.filter(t => t.id !== id));
    }, 3000);
  };

  return (
    <>
      <button onClick={() => addToast("Saved!")}>Save</button>

      <div className="fixed bottom-4 right-4 flex flex-col gap-2">
        <AnimatePresence>
          {toasts.map(toast => (
            <motion.div
              key={toast.id}
              className="bg-gray-900 text-white px-4 py-3 rounded-lg shadow-lg"
              initial={{ opacity: 0, x: 50, scale: 0.95 }}
              animate={{ opacity: 1, x: 0, scale: 1 }}
              exit={{ opacity: 0, x: 50, scale: 0.95 }}
              transition={{ duration: 0.2 }}
            >
              {toast.message}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </>
  );
}

useAnimate for Imperative Sequences

For complex sequential animations or animations triggered at specific moments, useAnimate gives you imperative control:

tsx
"use client";
import { useAnimate } from "motion/react";

function SubmitButton() {
  const [scope, animate] = useAnimate();

  const handleSubmit = async () => {
    // 1. Press feedback
    await animate(scope.current, { scale: 0.95 }, { duration: 0.1 });
    await animate(scope.current, { scale: 1 }, { duration: 0.1 });

    // 2. Loading state
    animate(scope.current, { opacity: 0.7 });

    try {
      await submitForm();

      // 3. Success feedback
      await animate(scope.current, { backgroundColor: "#22c55e" }, { duration: 0.3 });
      await animate(scope.current, { backgroundColor: "#3b82f6" }, { duration: 0.5 });
    } catch {
      // 4. Error shake
      await animate(scope.current, { x: [-8, 8, -8, 8, 0] }, { duration: 0.4 });
    }
  };

  return (
    <button ref={scope} onClick={handleSubmit} className="px-6 py-3 bg-blue-500 rounded-lg">
      Submit
    </button>
  );
}

animate returns a Promise, so you can await each step and chain them sequentially.

Layout Animations with layoutId

layoutId smoothly animates an element from one position to another — even across different component renders:

tsx
"use client";
import { motion, LayoutGroup } from "motion/react";
import { useState } from "react";

function TabNav() {
  const [activeTab, setActiveTab] = useState("home");
  const tabs = ["home", "about", "contact"];

  return (
    <LayoutGroup>
      <nav className="flex gap-4 relative">
        {tabs.map(tab => (
          <button
            key={tab}
            onClick={() => setActiveTab(tab)}
            className="relative px-4 py-2"
          >
            {tab}
            {activeTab === tab && (
              <motion.div
                layoutId="active-indicator"
                className="absolute inset-0 bg-blue-100 rounded-md -z-10"
              />
            )}
          </button>
        ))}
      </nav>
    </LayoutGroup>
  );
}

When active-indicator moves to a different tab, Framer Motion automatically animates it from the old position to the new one. No manual coordinate tracking needed.

Performance Best Practices

Stick to Transform and Opacity

tsx
// ❌ Causes layout reflow — expensive
<motion.div animate={{ width: "100%", height: 200, top: 50 }} />

// ✅ GPU-accelerated — no layout reflow
<motion.div animate={{ scaleX: 1, scaleY: 1, y: 50, opacity: 1 }} />

When you animate properties like width, height, or top, the browser recalculates layout for every frame. Sticking to transform and opacity keeps animations in the GPU and off the main thread.

Reduce Motion for Accessibility

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

function AnimatedCard({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.4 }}
    >
      {children}
    </motion.div>
  );
}

useReducedMotion respects the user's OS-level "Reduce Motion" preference (the prefers-reduced-motion media query). Always consider this for production apps.

FAQ

What's the difference between Framer Motion and Motion?

Motion is the new name for Framer Motion. In 2025, the library became an independent project under the motion package on npm. The API is identical — you only need to change your import from "framer-motion" to "motion/react". The old framer-motion package still installs but is no longer actively developed.

Do I need "use client" for every animated component in Next.js?

Yes. Any file that imports from motion/react and uses motion components needs the "use client" directive in Next.js App Router. The best practice is to keep your page-level components as Server Components and extract only the animated sections into small Client Components.

How do I animate a component when it unmounts?

Wrap it with AnimatePresence and add an exit prop. Without AnimatePresence, React removes the component instantly from the DOM and exit is ignored.

Is Motion heavy? How much does it add to my bundle?

The core of Motion is around 32 KB gzipped (tree-shakeable). If you only use basic animations (motion.div with initial/animate), the actual shipped code is smaller thanks to tree-shaking. For comparison, a full CSS animation library like GSAP is similar in size but doesn't integrate with React state.

Can I use Motion with React Server Components directly?

No. Motion components are client-side by nature — they require browser APIs and React state. Use them inside "use client" components. Server Components handle data fetching and static rendering; Client Components handle interactivity and animation.

What's the best way to stagger a list of items?

Use variants with staggerChildren on the parent container. This is more maintainable than manually setting delay on each item. A staggerChildren value of 0.05–0.1 seconds feels natural for most lists.

How do I handle prefers-reduced-motion?

Use the useReducedMotion hook. It returns true when the user has enabled "Reduce Motion" in their OS settings. Set duration: 0 or skip transform animations accordingly — keep opacity changes for content visibility.

Wrapping Up

Core Motion patterns in one table:

PatternUse CaseKey Props
Basic animationFade-in, slideinitial, animate, transition
Hover / tapInteractive feedbackwhileHover, whileTap
VariantsList stagger, coordinated animationsvariants, staggerChildren
Scroll-triggeredContent revealwhileInView, viewport
Mount / unmountModals, toastsAnimatePresence, exit
Sequential controlComplex choreographyuseAnimate
Layout changesTabs, list reorderinglayoutId, LayoutGroup

The most important rule: don't over-animate. opacity + y covers 80% of cases beautifully. Start simple, reach for complex patterns only when needed, and always keep transform/opacity as your primary animation properties for performance. Check the Motion documentation and changelog for the latest features and updates.

Related articles: