View Transitions vs JS Libraries

Part of View Transitions API Implementation in Modern View Transitions & Scroll APIs.

The problem

Animated transitions between views used to demand a JavaScript library: something to freeze the outgoing DOM, tween it against the incoming DOM, and manage the teardown. The native View Transitions API now performs the snapshot-and-crossfade choreography in the browser, with no runtime bundle. The decision is no longer “which library” but “library or platform” — and the answer turns on bundle cost, where the animation runs, how it degrades, and how much bespoke control you genuinely need.

Root-cause analysis: two different execution models

A JavaScript page-transition library operates on the live DOM. It reads geometry (often forcing synchronous layout), applies inline styles or Web Animations, and schedules teardown on completion. Every frame it drives passes through the main thread at least at setup, and any library that animates top, left, width or height incurs layout on each tick — the same layout thrashing risk as any hand-written animation.

Cost profile of the two approachesThe platform API trades choreography freedom for zero bytes and a compositor-only frame path.Cost profile of the two approachesView Transitions APIZero bytes on the critical pathSnapshots animate on the compositorShared-element morph is declarativeUnsupported browsers do an instant swapBest for crossfades and route changesJavaScript transition libraryBundle parse and compile before first usePer-frame main-thread work if it touches layoutFLIP measurement written by handFull timeline and physics controlReach for it only for bespoke choreography
The platform API trades choreography freedom for zero bytes and a compositor-only frame path.

The View Transitions API operates on snapshots. When you call document.startViewTransition(updateCallback), the browser captures the old view as an image, runs your DOM update, captures the new view, then animates between the two using a tree of ::view-transition pseudo-elements. Because these pseudo-elements are just images being crossfaded and transformed, driving them with transform and opacity keeps the animation on the compositor thread. The DOM update itself is synchronous and unanimated; only the pseudo-element layer moves. This is the same compositor discipline described across scroll-driven animation patterns — the properties you animate decide the thread, not the API.

Decision matrix

Dimension-by-dimension comparisonOnly the last two rows favour a library on a typical content site.Dimension-by-dimension comparisonView Transitions APIJS libraryBundle cost0 bytesParse + executeThread for theanimationCompositorMain thread setupProgressiveenhancementInstant swap fallbackNeeds the bundleShared-elementmorphDeclarativeManual FLIPBespokechoreographyLimited to CSSUnrestrictedMaintenanceTracks the platformDependency to patch
Only the last two rows favour a library on a typical content site.
Dimension View Transitions API JS page-transition library
Bundle size 0 bytes — ships with the browser Adds parse/compile/execute cost to the critical path
Execution thread Compositor for transform/opacity on the snapshot pseudo-elements Main thread at setup; per-frame main-thread work if it animates layout
Progressive enhancement Feature-detect; unsupported browsers do an instant swap Runs everywhere but only if the bundle has loaded and parsed
Shared-element morph First-class via view-transition-name Manual FLIP measurement and tweening
Bespoke choreography Limited to what pseudo-element CSS can express Full timeline control, physics, per-element sequencing
Maintenance Tracks the platform; no dependency upgrades Dependency to patch, audit and keep current
Debuggability Standard CSS/Animations panel Library-specific internals
Best fit Crossfades, shared-element morphs, route changes Complex, non-standard motion the declarative API cannot express

Read top to bottom: unless a row in the right-hand column names a capability you actually require — typically bespoke choreography or physics — the native API wins on cost and thread behaviour.

Production code

The snippet uses the native API with a clean progressive-enhancement fallback and a reduced-motion gate. Unsupported browsers get an instant, correct swap; supported browsers get a compositor-driven crossfade.

Shipping the platform API with a graceful fallbackThe same handler serves both browsers; only the animation is conditional.Shipping the platform API with a graceful fallback1Feature-detect with document.startViewTransition before wiring any transition-specific CSS.2Run the DOM update inside the callback and nowhere else.The browser captures a correct after-state3Express the motion entirely in ::view-transition-old and -new keyframes.Transform and opacity only, so it composites4Gate the keyframes behind prefers-reduced-motion and let the swap happen instantly.Reduced-motion users get no crossfade
The same handler serves both browsers; only the animation is conditional.
// Intent: animate a route change with the native API, degrade to an instant
// swap where unsupported, and honour reduced-motion preferences.

function navigate(updateDOM) {
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // Fallback path: no API support, or the user asked for reduced motion.
  // Either way, apply the DOM change immediately with no animation.
  if (!document.startViewTransition || reduce) {
    updateDOM();
    return;
  }

  // Enhanced path: the browser captures before/after snapshots and
  // crossfades the ::view-transition pseudo-elements on the compositor.
  document.startViewTransition(() => updateDOM());
}
/* Intent: drive the snapshot pseudo-elements with compositor-safe properties. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 240ms;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  /* default UA keyframes animate opacity — compositor-safe */
}

/* A shared element morphs by name; transform stays on the compositor. */
.hero-image {
  view-transition-name: hero;
}

/* Accessibility gate: no cross-view motion for reduced-motion users. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Rendering Impact: opacity + transform on ::view-transition-* pseudo-elements — composite only. The DOM update inside startViewTransition is a single synchronous, un-animated pass; no per-frame layout is scheduled.

Verification checklist

Migrating off a transition library

Most codebases do not need a rewrite to move across; they need to delete a layer. The library is usually doing four separable jobs, and only one of them is hard to give up.

It is intercepting navigation, measuring element positions, tweening between them, and orchestrating the order in which pieces move. The first job stays — a router still has to know a navigation happened. The second and third are exactly what the platform now does for free: the snapshot pair replaces FLIP measurement, and the compositor replaces the tween loop. The fourth, orchestration, is the one that has to be re-expressed as CSS rather than removed, and the honest answer is that some sequences cannot be. A stagger across twelve list items with per-item delays is straightforward; a sequence whose timing depends on runtime measurement is not.

A sensible migration order is: replace crossfades first (they are one CSS rule), then shared-element morphs (they are two view-transition-name assignments), and only then decide whether the remaining bespoke sequences justify keeping the dependency for one route. In most products the answer is that two or three routes keep custom motion and everything else moves to the platform, which still removes the library from the critical path for the majority of navigations.

Constraints and trade-offs

  • The native API’s declarative surface cannot express arbitrary timelines; genuinely bespoke choreography (staggered physics, interruptible drags) still needs JavaScript.
  • Snapshots are raster images: very large or continuously updating regions can be memory-heavy to capture, so scope view-transition-name to the elements that must morph.
  • Cross-document transitions require same-origin navigation and the @view-transition opt-in; see implementing cross-document view transitions in SPAs.
  • A JS library remains defensible when you must support a browser matrix where the native API is unavailable and the animation is non-negotiable — but measure the bundle cost against an instant swap first.
  • Animating layout properties on the pseudo-elements reintroduces main-thread work; keep to transform and opacity.