mrkeyoor.com_
Thu 06 Aug 08:55 UTC
npmWeb Frontendupdated 06 Aug 2026

motion

Motion is the animation library formerly published as Framer Motion. It has two faces. From "motion" you get a small imperative API for plain JavaScript: animate(target, keyframes, options), plus scroll(), inView(), hover(), press(), and stagger(). From "motion/react" you get the declarative React API: motion.div and friends with initial, animate, exit, whileHover, whileTap and drag props, AnimatePresence for exit animations, and the layout prop that animates an element between two DOM positions it never explicitly moved through. Underneath, both sit on the same engine, which hands off to the browser's Web Animations API when a value is hardware-accelerable and falls back to a JavaScript loop when it is not. That hybrid is the reason it can do springs, layout projection, and scroll-linked effects that CSS transitions cannot, while still keeping simple opacity and transform animations off the main thread.

Verdict

The default pick for React apps that need real interaction design, and layout animations plus AnimatePresence are things you genuinely cannot rebuild cheaply. Just be honest about whether your fades and hovers actually need 44 KB, and budget for the fact that a new major landed in January 2025 and again in August 2026.

API stability3/5The core motion.div and animate() surfaces have been recognizable for years and majors are usually small, but the major number reached 13 on 2026-08-05, releases land weekly, and a full package rename from framer-motion to motion left most tutorials online importing from the wrong place; 13.0.0 removed the automatic @emotion/is-prop-valid integration
Docs4/5motion.dev is well organized with live editable examples for nearly every API and a changelog that names the exact behaviour that changed, but the split between free docs and Motion+ content means the deepest examples and tutorials are paywalled, and the vanilla JS docs are noticeably thinner than the React ones
Maintenance5/5Pushed 2026-08-05 with a major shipped the same day, sponsor-funded with Framer as the primary backer, a real changelog entry for every fix, and 103 open issues out of 109 open issues and PRs on a 33k-star project
Ecosystem5/5About 17.5M downloads a week under this name plus the ongoing framer-motion downloads, first-party packages for React, Vue, and plain JS, and it is the animation layer assumed by shadcn-style component collections and most React UI tutorials

Use it if

  • You are animating a React UI and want enter and exit transitions: AnimatePresence is the least painful way to animate a component that is about to unmount, and nothing in CSS gives you that
  • You need layout animations: add the layout prop and an element animates between two positions after a reflow, or use layoutId to make one element appear to morph into another across components
  • You want springs described in terms you can reason about: transition: { type: 'spring', visualDuration: 0.4, bounce: 0.25 } instead of guessing stiffness and damping numbers
  • You need scroll-linked or viewport-triggered effects: useScroll and whileInView in React, scroll() and inView() in vanilla JS, both built on IntersectionObserver and scroll timelines rather than scroll event handlers
  • You are on Vue or plain JS and want the same mental model: motion-v and the "motion" entry point share the API shape, so patterns transfer
Skip it if

Setup reality

npm install motion, and then decide which entry point you are actually importing from, because that choice is most of the setup. "motion" is vanilla JS, "motion/react" is the React components, "motion/mini" is the WAAPI-only build, and "motion/react-m" is the stripped m component used with LazyMotion. Mixing them is the common mistake: importing animate from "motion" inside a React component works but bypasses React's lifecycle, and useAnimate from "motion/react" is what you actually wanted. React 18 or 19 is a peer dependency, declared optional so the vanilla install does not demand it, which means a wrong React version fails at runtime rather than at install. Everything in motion/react is client-side, so Next.js App Router needs 'use client' at the top of any file that touches it. If you are migrating, note the package is a thin re-export of framer-motion internals, so both packages installed at once gives you two copies of the engine and two sets of contexts, and layout animations quietly stop coordinating. On 13.0.0 the @emotion/is-prop-valid integration is gone: pass isValidProp to MotionConfig yourself if unknown props were being filtered for you before.

Patterns

Animate a DOM element without Reactanimate-element-vanilla

import { animate } from "motion"

const controls = animate(
  "#box",
  { x: 200, rotate: 90, backgroundColor: "#0368ff" },
  { duration: 0.6, ease: "easeOut" }
)

await controls
controls.stop()

The first argument takes a selector, an Element, or an array of them, so animating a list is one call. The returned object is thenable, so you can await completion, but it is not a Promise: calling .stop() after awaiting is a no-op rather than an error.

The declarative React componentreact-motion-component

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

export function Card() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 12 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3, ease: "easeOut" }}
      className="card"
    >
      Hello
    </motion.div>
  )
}

initial runs once on mount; passing initial={false} skips the mount animation entirely, which is what you want for content that is already on screen at page load. In Next.js App Router the 'use client' directive is mandatory here, and putting a motion component in a shared layout drags the engine into every route's client bundle.

Springs you can actually tunespring-transition

import { motion } from "motion/react"

<motion.div
  animate={{ scale: 1.1 }}
  transition={{ type: "spring", visualDuration: 0.4, bounce: 0.25 }}
/>

// the older, harder-to-reason-about form
<motion.div
  animate={{ scale: 1.1 }}
  transition={{ type: "spring", stiffness: 400, damping: 30, mass: 1 }}
/>

visualDuration is how long the animation looks like it takes to settle, and bounce is 0 to 1, which is far easier to tune than stiffness and damping. Springs ignore duration and ease, so a transition that mixes type: 'spring' with duration silently drops the duration unless you use visualDuration.

Hover, press, and drag as propsgestures-and-drag

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

function Draggable() {
  const bounds = useRef(null)
  return (
    <div ref={bounds} className="track">
      <motion.div
        drag="x"
        dragConstraints={bounds}
        dragElastic={0.2}
        dragSnapToOrigin
        whileHover={{ scale: 1.05 }}
        whileTap={{ scale: 0.95 }}
        onDragEnd={(_, info) => console.log(info.offset.x, info.velocity.x)}
      />
    </div>
  )
}

drag writes to transform, so the element moves without React re-rendering and your own state never sees it; read position from the info argument or from a motion value instead. dragConstraints against a ref is measured on drag start, so a container that resizes mid-gesture keeps the stale bounds.

Animate a component that is unmountingexit-animations

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

function Modal({ open }) {
  return (
    <AnimatePresence mode="wait" initial={false}>
      {open && (
        <motion.div
          key="modal"
          initial={{ opacity: 0, scale: 0.96 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.96 }}
        />
      )}
    </AnimatePresence>
  )
}

The child needs a stable key or AnimatePresence cannot tell a swap from a re-render, and the conditional has to be inside AnimatePresence, not around it. mode='wait' finishes the exit before the next child enters, which is right for route transitions and wrong for lists, where it serializes every removal.

Animate layout changes and morph between elementslayout-animations

import { motion, LayoutGroup } from "motion/react"

// element animates to its new size or position after any reflow
<motion.div layout transition={{ type: "spring", visualDuration: 0.3, bounce: 0 }} />

// same layoutId in two places: the first morphs into the second
<LayoutGroup>
  {items.map((item) => (
    <motion.li key={item.id} layout>
      {item.id === selected && <motion.div layoutId="highlight" className="pill" />}
      {item.label}
    </motion.li>
  ))}
</LayoutGroup>

Layout animations work by projecting transforms, so children get scaled with the parent unless they also carry layout. Border radius and box shadow distort under that scaling unless set as inline style props on the motion element. Only one element per layoutId may be mounted at a time or the morph target is ambiguous.

Tie an animation to scroll progressscroll-linked

import { motion, useScroll, useTransform } from "motion/react"
import { useRef } from "react"

function Parallax() {
  const ref = useRef(null)
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"],
  })
  const y = useTransform(scrollYProgress, [0, 1], ["0%", "-30%"])
  return <div ref={ref}><motion.img src="/hero.jpg" style={{ y }} /></div>
}

// vanilla equivalent
import { scroll, animate } from "motion"
scroll(animate("#hero", { opacity: [1, 0] }), { target: document.querySelector("#hero") })

scrollYProgress is a motion value, so passing it through style updates the DOM without re-rendering the component; reading .get() inside render instead gives you a stale number and no updates. The offset strings are element-edge to viewport-edge pairs, and getting them backwards is why parallax often looks inverted.

Drive animation from a value React never re-renders formotion-values

import { motion, useMotionValue, useSpring, useTransform, useMotionValueEvent } from "motion/react"

function Tilt() {
  const x = useMotionValue(0)
  const smooth = useSpring(x, { visualDuration: 0.3, bounce: 0.2 })
  const rotate = useTransform(smooth, [-200, 200], [-15, 15])

  useMotionValueEvent(rotate, "change", (v) => console.log(v))

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

Motion values live outside React state on purpose: setting one writes straight to the DOM and never triggers a render, which is what makes pointer-driven animation cheap. The cost is that anything else in your component reading x.get() will not update, so subscribe with useMotionValueEvent instead of reading during render.

Stagger a list and sequence multiple animationsstagger-sequence

import { animate, stagger } from "motion"

animate("li", { opacity: 1, y: 0 }, { delay: stagger(0.06, { startDelay: 0.2 }) })

// a sequence, with overlap via `at`
animate([
  ["#title", { opacity: 1, y: 0 }, { duration: 0.4 }],
  ["#subtitle", { opacity: 1 }, { duration: 0.3, at: "-0.2" }],
  ["#cta", { scale: [0.9, 1] }, { at: "<" }],
])

at takes a relative offset like '-0.2' to overlap with the previous step, '<' to start with it, or an absolute number in seconds. stagger() returns a function, so it goes in delay, not in a separate option. In React, useAnimate gives you a scoped animate that only touches elements inside its ref.

Name states once and propagate them to childrenvariants

import { motion } from "motion/react"

const list = {
  hidden: { opacity: 0 },
  show: { opacity: 1, transition: { staggerChildren: 0.05, delayChildren: 0.1 } },
}
const item = { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0 } }

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

Children inherit the variant label from the parent, which is why the li elements need no initial or animate of their own. Propagation stops the moment a child sets its own animate prop, and that silent break is the usual reason a stagger does nothing.

Cut the bundle with mini or LazyMotionreduce-bundle-size

// vanilla: WAAPI only, much smaller, no JS fallback
import { animate } from "motion/mini"
import { spring } from "motion"
animate("#box", { opacity: 1 }, { type: spring, visualDuration: 0.3 })

// React: ship the stripped `m` component, load features lazily
import { LazyMotion, domAnimation } from "motion/react"
import * as m from "motion/react-m"

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

motion/mini only animates what the Web Animations API can, so springs need the generator imported explicitly and independent transforms are limited. LazyMotion with strict throws if you import the full motion component anywhere inside it, which is the only reliable way to stop a teammate undoing the saving. domAnimation excludes layout animations; that needs domMax, which is most of the engine again.

Respect prefers-reduced-motionreduced-motion

import { MotionConfig, useReducedMotion, motion } from "motion/react"

// app-wide: transforms and layout animations are skipped, opacity still animates
<MotionConfig reducedMotion="user">
  <App />
</MotionConfig>

// per component, when you want a different treatment rather than none
function Panel() {
  const reduce = useReducedMotion()
  return <motion.div animate={reduce ? { opacity: 1 } : { opacity: 1, x: 0 }} />
}

reducedMotion='user' disables transform and layout animations but deliberately keeps opacity, since fades rarely trigger vestibular problems. This is opt-in: without MotionConfig, Motion animates everything regardless of the OS setting.

Alternatives

PackageRegistryPick it when
framer-motionnpmYou have an existing codebase on the old package name and do not want a rename churn; it is the same code, and motion depends on it
gsapnpmYou are orchestrating long timelines, SVG morphs, or scroll scenes across many elements and want a sequencing model rather than per-component props
@react-spring/webnpmYou want spring physics with a hooks-first API and no magic motion components, and you are happy to write the interpolation yourself
motion-vnpmYou are on Vue and want the same API shape as Motion for React