mrkeyoor.com_
Thu 06 Aug 02:46 UTC
npmWeb Frontendupdated 06 Aug 2026

framer-motion

framer-motion is the React animation library from the Motion project. Instead of writing keyframes or transition classes, you render motion.div (or motion.button, motion.path, and so on) and describe the state you want in props: initial, animate, exit, whileHover, whileTap, whileInView. It interpolates between those states with spring physics or duration-based easing, and it handles the things CSS cannot: animating an element out before React unmounts it, animating layout changes that CSS transitions ignore, and moving a shared element between two different components. Note the naming: the project renamed itself to Motion, the current package is called motion with a motion/react entry point, and framer-motion is the legacy name published from the same repo at the same version.

Verdict

The default answer for animation in React, and exit plus layout animations alone justify it on any app with real UI transitions. For new code install motion instead of framer-motion, and if all you need is a fade and a hover, write the CSS and save the 60 KB.

API stability4/5Majors land about once a year (11 in Jan 2024, 12 in Jan 2025, 13 in Aug 2026) but they are small: 13.0.0's only breaking change was dropping the optional @emotion/is-prop-valid dependency. The motion component props have been stable for years; the churn is in newer surfaces like animateView, which changed shape several times during 12.x.
Docs4/5motion.dev has live editable examples for nearly every API and a real React section, but the deepest material (330+ examples, 100+ tutorials, the transition editor) requires the paid Motion+ membership, and the free pages sometimes stop at a prop table without covering the failure modes.
Maintenance5/513.0.0 shipped 5 August 2026 with the repo pushed the same day, 12.x saw a release roughly every few weeks, and 103 open issues (109 counting PRs) is low for a 33k-star repo. Funded by Motion+ sales and Framer, so it is not a spare-time project.
Ecosystem5/5About 42M weekly downloads and 33k stars, assumed by most copy-paste React component collections, with a Vue port (motion-v) and a vanilla JS entry from the same codebase. The one wrinkle is that you will meet both framer-motion and motion/react imports in existing code.

Use it if

  • You need exit animations: React unmounts the element before CSS can transition it, and AnimatePresence is the standard fix for modals, toasts, and route changes
  • You want spring physics and interruptible animations, where a new target retargets from the current velocity instead of restarting a fixed-duration tween
  • You need shared element transitions, where a card grows into a detail view: layoutId does this in two props and hand-rolling it means measuring and FLIP-ing yourself
  • You have gesture-driven UI (drag to dismiss, pull to refresh, sliders) and want a MotionValue you can read, transform, and spring without re-rendering React on every frame
  • You want scroll-linked effects with useScroll and useTransform rather than writing scroll listeners and rAF loops
Skip it if

Setup reality

npm install framer-motion, or npm install motion for the current name. React 18 or 19 is a peer dependency (declared optional, because the same repo also ships a vanilla JS entry point), and TypeScript types are bundled. Two things catch people immediately. In Next.js App Router every file rendering a motion component needs "use client" at the top, since the whole library depends on hooks and effects; the error you get otherwise is a confusing server-component complaint about function props. And v13 removed the bundled optional @emotion/is-prop-valid dependency, so if you wrap a styled-components or Emotion component with motion() and want unknown props filtered out, you now install @emotion/is-prop-valid yourself and pass it through <MotionConfig isValidProp={isPropValid}>. Beyond that, budget for bundle size: importing the top-level motion component pulls in every feature, and cutting that down means restructuring around LazyMotion and the m component.

Patterns

Animate an element inanimate-on-mount

import { motion } from 'framer-motion';

<motion.div
  initial={{ opacity: 0, y: 12 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.25, ease: 'easeOut' }}
>
  Hello
</motion.div>

Omitting transition gives you a spring for physical values like x and y, and a tween for opacity and color. In Next.js App Router this file needs "use client" at the top or the build fails on function props crossing the server boundary.

Animate an element out before unmountexit-animation

import { AnimatePresence, motion } from 'framer-motion';

<AnimatePresence mode="wait">
  {isOpen && (
    <motion.div
      key="dialog"
      initial={{ opacity: 0, scale: 0.96 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.96 }}
    >
      <Dialog />
    </motion.div>
  )}
</AnimatePresence>

This is the reason most teams install the library at all, since CSS cannot animate an element React has already removed. The child needs a stable key, and AnimatePresence has to stay mounted itself, so putting the conditional above it instead of inside it silently disables the exit. mode="wait" makes the outgoing element finish before the incoming one starts.

Stagger a list with variantsvariants-stagger

const list = {
  hidden: { opacity: 0 },
  show: { opacity: 1, transition: { staggerChildren: 0.05 } },
};
const item = {
  hidden: { opacity: 0, x: -8 },
  show: { opacity: 1, x: 0 },
};

<motion.ul variants={list} initial="hidden" animate="show">
  {rows.map((r) => (
    <motion.li key={r.id} variants={item}>{r.label}</motion.li>
  ))}
</motion.ul>

Variant labels propagate down the tree automatically, so children need variants but not their own initial or animate. Propagation stops the moment a child sets animate explicitly, which is the usual reason a stagger does nothing.

Hover, tap, and in-view statesgesture-states

<motion.button
  whileHover={{ scale: 1.04 }}
  whileTap={{ scale: 0.97 }}
  whileFocus={{ outlineColor: '#4f46e5' }}
  transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
  Save
</motion.button>

<motion.section
  initial={{ opacity: 0 }}
  whileInView={{ opacity: 1 }}
  viewport={{ once: true, amount: 0.3 }}
/>

whileHover uses pointer events, so it does not stick on touch devices the way CSS :hover does. whileInView is backed by IntersectionObserver; without viewport={{ once: true }} the animation replays every time the element scrolls back into view.

Animate layout changeslayout-animation

<motion.div layout transition={{ type: 'spring', stiffness: 300, damping: 30 }}>
  {expanded ? <FullDetail /> : <Summary />}
</motion.div>

// text-heavy boxes: animate position only, not scale
<motion.div layout="position">{label}</motion.div>

Layout animation measures before and after, then interpolates with transforms, so a box changing width visibly stretches its text and border radius during the animation. layout="position" avoids the stretch by animating only the offset. Any CSS transition on the same element will fight the transform and produce jitter.

Move an element between two componentsshared-element

// in the grid
<motion.div layoutId={`card-${id}`} onClick={() => select(id)}>
  <Thumb />
</motion.div>

// in the expanded view, rendered somewhere else entirely
{selected && (
  <motion.div layoutId={`card-${selected}`}>
    <FullCard />
  </motion.div>
)}

Only one element with a given layoutId may be mounted at a time; two live copies makes the transition jump. Wrap both in LayoutGroup if they live in separate trees that re-render independently.

Drive a value from scroll positionscroll-linked

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

const ref = useRef(null);
const { scrollYProgress } = useScroll({
  target: ref,
  offset: ['start end', 'end start'],
});
const opacity = useTransform(scrollYProgress, [0, 0.4, 1], [0, 1, 0]);

<motion.div ref={ref} style={{ opacity }} />

scrollYProgress is a MotionValue, so passing it through style updates the DOM directly without re-rendering the component. Reading it with .get() inside render gives you a stale value; use useMotionValueEvent if you need to react in JS.

Smooth a value with a springmotion-value-spring

import { useMotionValue, useSpring, useTransform } from 'framer-motion';

const x = useMotionValue(0);
const smoothX = useSpring(x, { stiffness: 200, damping: 30 });
const rotate = useTransform(smoothX, [-200, 200], [-15, 15]);

<motion.div style={{ x: smoothX, rotate }} onPointerMove={(e) => x.set(e.clientX - 200)} />

MotionValues live outside React state, so updating one at pointer-event rate costs no re-renders. That also means nothing in your component tree knows the value changed, so anything derived from it must go through useTransform or useMotionValueEvent.

Drag with bounds and snap-backdrag-constraints

const container = useRef(null);

<div ref={container}>
  <motion.div
    drag
    dragConstraints={container}
    dragElastic={0.15}
    dragMomentum={false}
    onDragEnd={(event, info) => {
      if (info.offset.x < -120) dismiss();
    }}
  />
</div>

dragConstraints can be a ref or an object of pixel bounds; the ref form re-measures on layout change but has historically needed care when the container scrolls. info.offset is the distance from where the drag started, while info.point is viewport coordinates, and mixing them up is the usual swipe-to-dismiss bug.

Animate imperatively in a sequenceimperative-animate

import { useAnimate, stagger } from 'framer-motion';

const [scope, animate] = useAnimate();

async function shake() {
  await animate(scope.current, { x: [0, -8, 8, 0] }, { duration: 0.3 });
  await animate('li', { opacity: 1 }, { delay: stagger(0.05) });
}

<ul ref={scope}>{items}</ul>

Selector strings passed to animate are scoped to the ref, so 'li' only matches inside that subtree. useAnimate works on plain DOM elements too, which is the escape hatch when you cannot convert a third-party component into a motion component.

Ship less JavaScript with LazyMotionreduce-bundle-size

import { LazyMotion, domAnimation, m } from 'framer-motion';

<LazyMotion features={domAnimation} strict>
  <m.div animate={{ opacity: 1 }} />
</LazyMotion>

// layout animations and drag need the bigger bundle:
// <LazyMotion features={() => import('./features').then((r) => r.default)} />

m is the same component with no features baked in, so importing motion anywhere in the app defeats the whole exercise; strict makes that a runtime error instead of a silent regression. domAnimation excludes layout animations and drag, which live in the larger domMax feature set.

Honour the reduced-motion preferencerespect-reduced-motion

import { MotionConfig, useReducedMotion } from 'framer-motion';

<MotionConfig reducedMotion="user">
  <App />
</MotionConfig>

function Card() {
  const reduce = useReducedMotion();
  return <motion.div animate={{ y: reduce ? 0 : -8, opacity: 1 }} />;
}

reducedMotion="user" disables transform and layout animations while still allowing opacity and colour changes, which keeps state changes visible. It does not touch animations you drive yourself from a MotionValue, so check useReducedMotion in those paths too.

Alternatives

PackageRegistryPick it when
motionnpmAny new project: same library, current name, and the entry point the docs are written against
gsapnpmTimeline choreography, SVG morphing, or scroll scrubbing on pages that are not built with React
react-springnpmYou want spring-based animation with a hooks-first API and no magic motion.* element wrappers
@formkit/auto-animatenpmYou only want list add, remove, and reorder to animate, and you want it in one line with nothing to learn