Skip to main content
motion

Motion for React, View Transitions, and the New Animation Stack

Browser animation capabilities have caught up to most use cases. Here's how Motion for React, View Transitions, and CSS animations divide the work in 2026.


7 min read

In 2026, you have three distinct tools for animation in a React project: Motion for React (the library formerly named Framer Motion), the View Transitions API, and CSS. Each one has a lane. The mistake most projects make is using only one of them — reaching for Motion when a CSS @starting-style would do the job, or trying to force the View Transitions API through a single-page app that doesn't benefit from it. The animation stack works best when each layer does what it's actually designed for.


What Motion for React owns

Motion for React handles three things better than anything else in the browser: layout animations, gesture-driven motion, and presence animations.

Layout animations are where Motion's value is clearest. When an element's position or size changes — a card expands, a list reorders, a sidebar collapses — layoutId and the layout prop handle the FLIP technique automatically. Without Motion, you'd calculate getBoundingClientRect before and after a DOM change, then animate the delta. Motion does this for you with one prop.

// A list that animates reorders automatically
import { motion, AnimatePresence } from "motion/react";

function SortableList({ items }) {
  return (
    <ul>
      <AnimatePresence>
        {items.map((item) => (
          <motion.li
            key={item.id}
            layout
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            transition={{ type: "spring", stiffness: 300, damping: 30 }}
          >
            {item.name}
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}

Gesture-driven motion — drag, hover with velocity, pan — is the second lane. The useDragControls, useMotionValue, and useTransform hooks give you physical-feeling interaction that CSS transitions can't replicate because CSS has no concept of pointer velocity or inertia.

Presence animations via AnimatePresence handle the hardest timing problem in UI animation: coordinating the unmounting of elements. React removes elements immediately. AnimatePresence defers the unmount until the exit animation completes, which is what makes modals and toasts feel finished instead of abrupt.

Spring physics is the connective tissue. Most CSS easing is bezier-based; Motion's springs respond to interruption. If a user reverses a gesture midway, a spring-based animation reverses naturally from its current velocity instead of jumping to a new starting point.


What the View Transitions API owns

The View Transitions API handles cross-document navigation and shared element transitions — the kind of animation where element A in page 1 appears to physically travel to its new position on page 2.

In a multi-page app (or any app using the browser's navigation API), this is now native browser behavior:

// Opt a specific element into a shared transition
// In your CSS:
// .card { view-transition-name: card-42; }

// Navigation triggers the transition automatically
// No JavaScript animation loop required
document.startViewTransition(() => {
  // Update DOM or trigger navigation
});

For a React Router or Next.js app, <ViewTransition> wrapper components are now available in both frameworks. React itself shipped first-class support for coordinating startViewTransition with its render cycle in React 19.

The View Transitions API is also handling the case that Motion has always struggled with: transitions that cross route boundaries. When you navigate from /products to /products/42, you can animate the product card into the detail view without any shared state between the two route components. The browser captures screenshots of the old and new states and interpolates between them using your CSS view-transition-name assignments.

The limitation is that the API animates between DOM snapshots. It doesn't know about velocity, it doesn't respond to gestures, and it can't animate layout changes that happen within a single page load. That's still Motion's territory.


What CSS owns in 2026

CSS handles far more than it did two years ago. Three specific additions are worth knowing:

@starting-style defines the "from" state of an element entering the DOM — the thing you previously needed a library for. A tooltip appearing, a popover opening, a display: block element fading in: these are now solvable with two CSS rules.

.popover {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 200ms, transform 200ms;
}

@starting-style {
  .popover {
    opacity: 0;
    transform: translateY(4px);
  }
}

Scroll-driven animations via animation-timeline: scroll() and animation-timeline: view() handle the entire category of scroll-linked effects without JavaScript. A reading progress bar, a sticky header that changes opacity on scroll, a card that fades in as it enters the viewport — all CSS, zero scroll event listeners, no IntersectionObserver boilerplate.

Discrete transitions — animating display, visibility, and overlay — are now supported via the transition-behavior: allow-discrete property. The display: none barrier that forced developers to keep elements in the DOM just to animate them out is no longer the wall it was.


The pattern of using all three together

A real project doesn't choose one. The stack looks like this:

  • CSS handles enter/exit for elements that don't need gesture response or layout tracking: tooltips, popovers, notifications, scroll-linked parallax.
  • Motion handles anything with gesture input, spring physics, layout shifts, or coordinated presence animations.
  • View Transitions handles page-to-page navigation and shared element transitions at route boundaries.

The layers are mostly additive. Where they conflict is at the seam between AnimatePresence and startViewTransition.


The AnimatePresence + View Transitions gotcha

Both AnimatePresence and startViewTransition want to control the timing of DOM updates. When a navigation triggers a view transition while AnimatePresence is also trying to run an exit animation, the two systems step on each other. The view transition captures a snapshot before AnimatePresence has finished, so the old page snapshot includes an element mid-exit.

The current working pattern is to avoid using AnimatePresence on elements that will be captured by a view transition snapshot. If a card is going to be a shared element in a route transition, animate its entry and exit with CSS @starting-style and the view transition's ::view-transition-old / ::view-transition-new pseudo-elements — not with AnimatePresence. Reserve AnimatePresence for in-page presence animations that don't cross a navigation boundary.

This is the roughest edge in the current stack. The Motion team has a proposal for a useViewTransition hook that would coordinate the two systems, but as of mid-2026 you're managing the boundary manually.


Choosing by signal, not by default

The default should not be "add Motion to everything." The motion/react bundle is not trivial, and most static page transitions don't need spring physics. Start with CSS. Reach for Motion when you need gesture response, layout tracking, or coordinated presence. Reach for the View Transitions API when you're navigating between routes and want the browser to handle shared element continuity.

The measure of a good animation is whether the user notices the absence of it — not whether they notice its presence. That standard applies equally to all three layers of the stack. Pick the smallest tool that satisfies the constraint, and the animation will feel right precisely because it didn't try too hard.