Type something to search...
Animations in React with Motion (Framer Motion)

Animations in React with Motion (Framer Motion)

CSS transitions get you far, until you need to animate something leaving the page. React removes the element from the DOM immediately, so there's nothing left to fade out. The same wall shows up with list reordering, elements that change size, shared transitions between two components, and gestures like drag. You end up juggling timeouts, extra state, and onTransitionEnd handlers.

Motion (the library formerly called Framer Motion) solves these problems with a declarative API. You describe the state an element should be in, and Motion animates there, including when it mounts, unmounts, or moves in the layout. In 2024 the project became independent and was renamed. The package is now motion and the React API imports from motion/react, but it's the same library and the same API.

This post covers installing Motion, the core animate and transition props, variants and staggered children, exit animations with AnimatePresence, layout animations, gestures, scroll-driven effects, imperative animations, and how to respect users who prefer reduced motion.

Installing Motion

npm install motion

If you're migrating from framer-motion, replace the package and update imports from "framer-motion" to "motion/react". The APIs are compatible.

The motion Component

Every HTML and SVG element has a motion counterpart: motion.div, motion.button, motion.li, motion.svg, motion.path, and so on. They accept all normal props plus animation props.

import { motion } from "motion/react";

export function FadeIn() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4, ease: "easeOut" }}
    >
      Hello there
    </motion.div>
  );
}
  • initial is the state on first render.
  • animate is the target state. Whenever these values change, Motion animates to the new values.
  • transition controls how it gets there.

x, y, scale, and rotate are shorthand transforms that Motion composes into a single transform. Animating transforms and opacity is the most performant choice, because browsers can run them without triggering layout.

Animating From State

Because animate is just a prop, you drive animations with ordinary React state:

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

export function Toggle() {
  const [on, setOn] = useState(false);

  return (
    <button
      type="button"
      role="switch"
      aria-checked={on}
      onClick={() => setOn((v) => !v)}
      className="switch"
      style={{ justifyContent: on ? "flex-end" : "flex-start" }}
    >
      <motion.span
        layout
        className="knob"
        transition={{ type: "spring", stiffness: 500, damping: 30 }}
      />
    </button>
  );
}

The knob jumps between flex-start and flex-end, and the layout prop tells Motion to animate that position change smoothly. More on layout shortly.

Transitions: Tweens and Springs

Motion supports two main kinds of transitions:

  • Tween: duration-based with an easing curve. Good for opacity and color.
  • Spring: physics-based, defined by stiffness, damping, and mass, or by visualDuration and bounce. Springs feel natural for movement and handle interruption gracefully, because they carry velocity into the next animation.
<motion.div
  animate={{ x: 100 }}
  transition={{ type: "spring", visualDuration: 0.4, bounce: 0.25 }}
/>

You can set transitions per property, which is useful when opacity should be quick but movement should be springy:

<motion.div
  animate={{ opacity: 1, y: 0 }}
  transition={{
    opacity: { duration: 0.2 },
    y: { type: "spring", stiffness: 300, damping: 24 },
  }}
/>

Variants and Staggered Children

Variants are named animation states. Define them once in an object, then refer to them by name. Their real power is propagation: when a parent switches variants, every motion child with the same variant names switches too, and the parent can stagger them.

import { motion, type Variants } from "motion/react";

const list: Variants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.08, delayChildren: 0.1 },
  },
};

const item: Variants = {
  hidden: { opacity: 0, y: 12 },
  visible: { opacity: 1, y: 0 },
};

export function FeatureList({ features }: { features: string[] }) {
  return (
    <motion.ul variants={list} initial="hidden" animate="visible">
      {features.map((feature) => (
        <motion.li key={feature} variants={item}>
          {feature}
        </motion.li>
      ))}
    </motion.ul>
  );
}

The children don't need initial or animate props. They inherit the active variant name from the parent and play in sequence 80ms apart. This keeps orchestration in one place instead of computing a delay for each item.

Exit Animations With AnimatePresence

When React unmounts a component, it's gone. AnimatePresence changes that. It keeps a removed child in the DOM long enough to play its exit animation, then removes it.

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

export function Notice() {
  const [visible, setVisible] = useState(true);

  return (
    <div>
      <button onClick={() => setVisible((v) => !v)}>Toggle</button>
      <AnimatePresence>
        {visible && (
          <motion.div
            key="notice"
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: "auto" }}
            exit={{ opacity: 0, height: 0 }}
            style={{ overflow: "hidden" }}
          >
            <p>Your changes were saved.</p>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

Two rules make AnimatePresence work:

  1. It must stay mounted. Put the condition inside it, not around it.
  2. Direct children need a stable, unique key so Motion can track which ones left.

Notice height: "auto". Animating to and from auto is something CSS transitions can't do, and Motion measures it for you.

Switching Between Elements

Change the key to make AnimatePresence treat content as a new element. With mode="wait", the old one exits before the new one enters, which is ideal for step-by-step flows and tab panels:

import { AnimatePresence, motion } from "motion/react";

type Step = { id: string; title: string; body: string };

export function StepPanel({ step }: { step: Step }) {
  return (
    <AnimatePresence mode="wait">
      <motion.section
        key={step.id}
        initial={{ opacity: 0, x: 24 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: -24 }}
        transition={{ duration: 0.2 }}
      >
        <h2>{step.title}</h2>
        <p>{step.body}</p>
      </motion.section>
    </AnimatePresence>
  );
}

Exit animations combine naturally with portals for modals and toasts. If you're building those, portals in React shows how to render them, and you can wrap the portaled content in AnimatePresence for enter and exit transitions.

Layout Animations

Layout changes are the hardest thing to animate by hand. An element changes size, a list reorders, or a grid reflows, and you want a smooth transition instead of a jump. Add the layout prop and Motion handles it using a technique called FLIP: it measures the element before and after the change, then animates the difference with transforms.

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

export function ExpandableCard({
  title,
  body,
}: {
  title: string;
  body: string;
}) {
  const [open, setOpen] = useState(false);

  return (
    <motion.article
      layout
      onClick={() => setOpen((v) => !v)}
      className="card"
      style={{ borderRadius: 12 }}
    >
      <motion.h3 layout="position">{title}</motion.h3>
      {open && (
        <motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
          {body}
        </motion.p>
      )}
    </motion.article>
  );
}

Setting borderRadius through style (rather than only in CSS) lets Motion correct for distortion while scaling. layout="position" on the heading animates only its position, so the text doesn't stretch.

Reordering Lists

Add layout to each list item and reordering animates automatically:

import { motion } from "motion/react";

type Task = { id: string; title: string; done: boolean };

export function TaskList({ tasks }: { tasks: Task[] }) {
  const sorted = [...tasks].sort((a, b) => Number(a.done) - Number(b.done));

  return (
    <ul>
      {sorted.map((task) => (
        <motion.li
          key={task.id}
          layout
          transition={{ type: "spring", bounce: 0.2 }}
        >
          {task.title}
        </motion.li>
      ))}
    </ul>
  );
}

Stable keys matter here more than anywhere. If keys are array indexes, Motion can't tell which element moved. Why keys matter in React lists explains the underlying reason.

Shared Element Transitions With layoutId

Give two different elements the same layoutId, and when one unmounts as the other mounts, Motion animates between them. A classic example is an active-tab indicator that slides between tabs:

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

const tabs = ["Overview", "Activity", "Settings"];

export function Tabs() {
  const [active, setActive] = useState(tabs[0]);

  return (
    <div role="tablist" className="tabs">
      {tabs.map((tab) => (
        <button
          key={tab}
          role="tab"
          aria-selected={active === tab}
          onClick={() => setActive(tab)}
          className="tab"
          style={{ position: "relative" }}
        >
          {active === tab && (
            <motion.span
              layoutId="tab-indicator"
              className="tab-indicator"
              transition={{ type: "spring", stiffness: 400, damping: 30 }}
            />
          )}
          <span style={{ position: "relative" }}>{tab}</span>
        </button>
      ))}
    </div>
  );
}

Only one indicator exists at a time, but because they share a layoutId, it appears to glide from tab to tab.

Gestures

Motion has gesture props for hover, tap, focus, and drag. The while props animate to a state while the gesture is active and back when it ends:

import { motion } from "motion/react";

export function LikeButton({ onLike }: { onLike: () => void }) {
  return (
    <motion.button
      type="button"
      onClick={onLike}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      whileFocus={{ boxShadow: "0 0 0 3px rgb(99 102 241 / 0.5)" }}
    >
      Like
    </motion.button>
  );
}

Drag is a single prop. Constrain it with pixel bounds or a ref to a container:

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

export function DragArea() {
  const constraintsRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={constraintsRef} className="drag-area">
      <motion.div
        drag
        dragConstraints={constraintsRef}
        dragElastic={0.2}
        whileDrag={{ scale: 1.1 }}
        className="draggable"
      />
    </div>
  );
}

For sortable lists and drop zones, a dedicated library is a better fit. See building drag-and-drop interfaces with dnd-kit.

Scroll Animations

Animate When Entering the Viewport

whileInView animates an element when it scrolls into view. viewport={{ once: true }} keeps it from replaying every time:

import { motion } from "motion/react";

export function Reveal({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, amount: 0.3 }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  );
}

Link Values to Scroll Position

useScroll returns motion values for scroll progress, and useTransform maps them to other values. Motion values update outside React's render cycle, so these animations don't re-render your component on every scroll event.

import { motion, useScroll, useSpring } from "motion/react";

export function ReadingProgress() {
  const { scrollYProgress } = useScroll();
  const scaleX = useSpring(scrollYProgress, { stiffness: 200, damping: 30 });

  return (
    <motion.div
      className="progress-bar"
      style={{
        scaleX,
        transformOrigin: "0%",
        position: "fixed",
        top: 0,
        left: 0,
        right: 0,
        height: 4,
      }}
    />
  );
}
import { useRef } from "react";
import { motion, useScroll, useTransform } from "motion/react";

export function ParallaxImage({ src }: { src: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"],
  });
  const y = useTransform(scrollYProgress, [0, 1], ["-15%", "15%"]);

  return (
    <div ref={ref} style={{ overflow: "hidden", height: 400 }}>
      <motion.img src={src} alt="" style={{ y, width: "100%" }} />
    </div>
  );
}

Imperative Animations With useAnimate

Sometimes you need to sequence animations in response to an event, like shaking a form on invalid submit. useAnimate gives you a scoped animate function:

import { useAnimate } from "motion/react";

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

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    if (!data.get("password")) {
      animate(scope.current, { x: [0, -10, 10, -6, 6, 0] }, { duration: 0.4 });
    }
  }

  return (
    <form ref={scope} onSubmit={handleSubmit}>
      <input name="password" type="password" aria-label="Password" />
      <button type="submit">Log in</button>
    </form>
  );
}

animate returns a promise-like animation, so you can await it and chain steps. Selectors passed to animate are scoped to the element in scope, so animate("button", { scale: 1.1 }) only affects buttons inside the form.

Respecting Reduced Motion

Some users enable "reduce motion" in their OS because large movement causes dizziness or nausea. Respecting that preference is an accessibility requirement, not a nice-to-have. The simplest approach is wrapping your app in MotionConfig:

import { MotionConfig } from "motion/react";

export function AppShell({ children }: { children: React.ReactNode }) {
  return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
}

With reducedMotion="user", Motion disables transform and layout animations when the user's OS requests reduced motion, while keeping opacity and color transitions. For custom behavior, useReducedMotion returns a boolean you can use to choose alternative animations.

Keeping the Bundle Small

The full motion component includes every feature. If bundle size matters, use LazyMotion with the lighter m components and load only the features you need:

import { LazyMotion, domAnimation } from "motion/react";
import * as m from "motion/react-m";

export function App() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div animate={{ opacity: 1 }} initial={{ opacity: 0 }}>
        Lighter bundle
      </m.div>
    </LazyMotion>
  );
}

domAnimation covers animations, variants, exit, and tap/hover/focus gestures. Use domMax if you also need drag and layout animations. Both can be loaded asynchronously with a dynamic import.

Common Mistakes With Motion

  • Wrapping AnimatePresence in the condition. It must stay mounted and contain the conditional child, or exit animations never run.
  • Missing or unstable keys. AnimatePresence and layout animations rely on keys to track elements. Index keys break them.
  • Animating width, height, or top when transforms would do. Layout properties trigger reflow every frame. Prefer x, y, and scale, or use the layout prop.
  • Ignoring reduced motion. Add MotionConfig with reducedMotion="user" at the root.
  • Storing scroll values in React state. Use motion values from useScroll and useTransform, which update without re-rendering.
  • Animating everything. Motion should guide attention. If every element bounces in, nothing stands out and the UI feels slow.

Frequently Asked Questions (FAQ) About Motion for React

They're the same library. Framer Motion became an independent project in 2024 and was renamed Motion. The package is now motion, and React components import from motion/react. The API is the same, so migrating is mostly a matter of changing the package and import paths.

Motion components need to run in the browser, so they must be used in Client Components. In frameworks with Server Components, add the use client directive to files that use motion components, or import from motion/react-client, which marks the components as client components for you.

Usually because AnimatePresence is unmounting along with the child, or the child has no stable key. Keep AnimatePresence always rendered, put the condition inside it, and give each direct child a unique key.

Not when used well. It animates transforms and opacity using hardware-accelerated paths where possible, and motion values update without React re-renders. Performance problems usually come from animating layout properties or animating many elements at once.

Use CSS transitions for simple hover and focus effects. Reach for Motion when you need exit animations, layout and reorder animations, shared element transitions, gestures like drag, springs, or scroll-linked effects. Mixing both in one app is normal.

Wrap your app in MotionConfig with reducedMotion set to user. Motion then skips transform and layout animations for users who have reduced motion enabled. For finer control, use the useReducedMotion hook to choose different animations.

Conclusion

Motion turns animation into a matter of describing states. initial, animate, and transition cover most needs. Variants coordinate groups and stagger children. AnimatePresence makes exit animations possible, layout and layoutId animate changes that would be painful by hand, gesture props handle hover, tap, and drag, and useScroll with useTransform drives scroll effects without re-renders.

Start small: add an exit animation to a toast or modal, then a layout prop to a list that reorders. Wrap the app in MotionConfig with reduced motion support from day one, and keep animations short and purposeful. Once those feel natural, explore layoutId for shared transitions, which is where Motion really sets itself apart from plain CSS.

Tags :
Share :

Related Posts

A Practical Guide to useEffect and Its Dependency Array

A Practical Guide to useEffect and Its Dependency Array

useEffect is the hook people get wrong most often, and the dependency array is usually where it goes wrong. Leave a value out and your effect works

Continue Reading
Accessibility Best Practices for React Developers

Accessibility Best Practices for React Developers

React makes it easy to build interfaces out of anything. A div with an onClick looks and behaves like a button for a mouse user, so it ships. The

Continue Reading
Authentication in React with JWT and Refresh Tokens

Authentication in React with JWT and Refresh Tokens

Most JWT tutorials end with localStorage.setItem("token", jwt) and an Authorization header on every request. It works on day one. Then the token

Continue Reading