Animations on the web in 2026 are in a quietly good place. The View Transitions API ships in every major browser, React 19.2 added a first-class `<ViewTransition>` component, Motion (the renamed Framer Motion) is smaller and faster, and Tailwind 4 made CSS-only micro-interactions trivial. The catch is that the three tools overlap enough that teams end up using the wrong one for the job — reaching for a 30 KB library to fade in a tooltip, or animating a route change with JavaScript when the browser would do it for free. This post maps each tool to the job it is actually best at, and covers the performance gotchas that separate animations that feel good from animations that regress Core Web Vitals.
Three tools, three jobs
There is no "one animation library to rule them all" in 2026, and the sooner a team stops looking for one, the better their UI ships. Each tool has a sweet spot; using them together is the point.
| Tool | Size on the wire | Sweet spot | Main thread cost | Watch out for |
|---|---|---|---|---|
| Motion / Framer Motion | ~18 KB gzip (tree-shakable) | Component choreography, gestures, layout animations | JS-driven; can stress main thread on big lists | INP regressions from runaway spring chains |
| View Transitions API | 0 KB (native) | Cross-route and across-view transitions | GPU-accelerated by the browser | 78% global support; needs a graceful fallback |
| CSS / Tailwind transitions | 0 KB beyond your CSS | Hover, focus, enter/leave of single elements | Compositor-only when animating transform/opacity | Animating width/height triggers layout |
| Web Animations API | 0 KB (native) | Imperative timelines without a library | Runs off the main thread for transform/opacity | Verbose for React; usually a Motion job |
This is the shape of the decision in 2026: one JS library for component-level work, one native API for across-the-page work, and CSS for everything small. If an app is reaching for a fourth tool, the odds are it is picking wrong on one of these three.
When Motion is the right answer
Motion — the library formerly known as Framer Motion — is the right pick when an animation depends on React state, gestures, or layout changes inside a single page. Drag-and-drop lists, an accordion that measures its own height, a card that flips on hover and respects `prefers-reduced-motion`, a modal that animates in and out with a spring: these are all Motion jobs. The `layout` prop alone pays for the bundle cost on any app that has a reorderable list or a masonry grid.
// motion — a card that animates in and respects reduced-motion
import { motion, useReducedMotion } from "motion/react";
export function Card({ children }: { children: React.ReactNode }) {
const reduce = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="rounded-2xl border p-6"
>
{children}
</motion.div>
);
}Two habits worth baking in. First, animate `transform` and `opacity` only where possible — both are composited off the main thread, which is the difference between 60 fps and a stutter. Second, wire `useReducedMotion` in from day one. Users who set that preference are not a rounding error, and ignoring it fails accessibility audits.
When the View Transitions API is the right answer
Route changes, tab changes, and shared-element transitions are what the View Transitions API was built for. The browser takes a snapshot of the outgoing view, snapshots the incoming view, and animates between them — no JavaScript animation loop, no component-tree hacks, no custom router integration. React 19.2's `<ViewTransition>` component makes this declarative in React; Next.js 16 wired it into the App Router so route transitions cost zero extra code.
// shared-element transition — a thumbnail that morphs into a hero image
// Next.js 16 + React 19.2
// List page
<Link href={`/cases/${caseId}`}>
<ViewTransition name={`case-hero-${caseId}`}>
<img src={thumb} alt={title} className="h-24 w-24 rounded-lg" />
</ViewTransition>
</Link>
// Detail page — same name, different size and position
<ViewTransition name={`case-hero-${caseId}`}>
<img src={hero} alt={title} className="h-96 w-full rounded-3xl" />
</ViewTransition>
// That's it. The browser animates the morph between the two snapshots.That snippet does something it would have taken 100 lines of Motion code to do three years ago, and it runs entirely on the compositor. The transition is GPU-accelerated, does not block the main thread, and scales to arbitrarily complex DOM without adding JavaScript work.
Global support for the View Transitions API is around 78% in early 2026 — mostly Firefox and older mobile browsers remain. Feature-detect with `document.startViewTransition` and fall back to an instant swap (no animation) for unsupported browsers. Do not fall back to a JS animation library; the cost there is not worth the fidelity.
When CSS is the right answer (which is most of the time)
Hover, focus, checkbox toggles, a sidebar that slides open, a button that grows on press, a skeleton loader pulsing: all CSS jobs. They cost zero bytes, run on the compositor if they animate `transform` and `opacity`, and are trivially testable because they are declarative. Tailwind 4's `@starting-style` support and the new `transition-behavior: allow-discrete` value mean that even popovers and dialogs can animate in and out from CSS alone — no JavaScript, no library.
<!-- a tooltip that animates in on hover, CSS only -->
<button class="group relative">
Save
<span
class="
pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2
scale-95 rounded-md bg-zinc-900 px-2 py-1 text-xs text-white opacity-0
transition-all duration-150 ease-out
group-hover:scale-100 group-hover:opacity-100
"
>
Ctrl + S
</span>
</button>No library. No JavaScript. Runs on the compositor. If a team finds themselves reaching for Motion to build something that shape, stop and check whether CSS can do it — the answer is usually yes, and the bundle gets smaller every time the answer is yes.
Performance gotchas that tank INP
Interaction to Next Paint (INP) is the Core Web Vital that animations most often damage. The threshold is 200 ms for a "good" score, and a single long animation frame can blow that budget. A few recurring culprits:
- Animating layout-triggering properties — width, height, top, left. These force the browser to re-layout the page on every frame. Use transform and opacity instead; Motion's `layout` prop does this for you when possible.
- Running dozens of spring animations at once on the same page. Each animation adds work to every frame. If a list has 100 rows and each row animates in, stagger them or switch to CSS.
- JavaScript work inside the animation's `onUpdate` callback — logging, analytics, DOM measurement. Anything heavier than math belongs outside the frame loop.
- Synchronously mounting a heavy component at the start of an animation. The component tree measures, the animation stutters. Render the new component first, then animate it.
- Over-long transitions. Anything past ~300 ms on enter/exit starts feeling sluggish rather than snappy. The right default is 150–250 ms.
JS-driven animations run on the main thread by default, which is the same thread that processes clicks. A spring chain that runs for 600 ms after a click is 600 ms where the next click has to wait. That is exactly the shape of an INP regression — and it is almost always invisible in local dev on a fast machine.
The decision framework
A two-minute version of the call the Vertex Stack team makes on every new UI surface:
- Is the animation tied to a route change or a view swap? View Transitions API. Skip the rest of this list.
- Is it a micro-interaction on a single element (hover, focus, active, open/closed)? CSS — usually Tailwind utilities — unless React state is driving it.
- Does it need to respond to gestures, measure layout, or animate between DOM reorders? Motion. This is the job the library is actually for.
- Is it a one-off imperative timeline with no React involvement? Web Animations API directly. Motion is overkill.
The failure mode to avoid is picking the tool by familiarity. A team that knows Motion well will animate route transitions in Motion because that is what they know — and pay the bundle and main-thread cost forever. Forty lines of reading the View Transitions API docs saves that cost on every route change for the life of the app.
Key takeaways
- Motion is for component choreography — gestures, layout animations, and anything tied to React state.
- The View Transitions API is for cross-route and shared-element animations — native, GPU-accelerated, zero bundle cost.
- CSS with Tailwind handles the long tail of micro-interactions and should be the default for anything hover, focus, or toggle-based.
- Animate transform and opacity; everything else risks layout thrash and INP regressions.
- Measure on a mid-tier phone, not a developer laptop. INP problems are almost always invisible on the hardware that writes the code.