Skip to main content
Ganesh Joshi
Back to Blogs

View Transitions API: smooth page changes in the browser

February 15, 20264 min read
Tutorials
Browser or UI on screen

The View Transitions API enables smooth, native-feeling transitions between DOM states or pages. Instead of jarring instant updates, users see animated changes that help them understand what changed. It's native browser capability—no JavaScript animation libraries needed.

How it works

When you trigger a view transition:

  1. Browser captures current visual state as a snapshot
  2. Your callback updates the DOM
  3. Browser captures new visual state
  4. Browser animates between old and new states
document.startViewTransition(() => {
  // Update the DOM
  document.getElementById('content').innerHTML = newContent;
});

The default animation is a cross-fade, but you can customize everything with CSS.

Browser support

Browser Support
Chrome 111+ Full
Edge 111+ Full
Opera 97+ Full
Safari 18+ Partial
Firefox In development

Always feature detect:

if (document.startViewTransition) {
  document.startViewTransition(() => updateDOM());
} else {
  updateDOM(); // Fallback: instant update
}

Basic usage

Simple cross-fade

function navigateTo(href) {
  if (!document.startViewTransition) {
    window.location.href = href;
    return;
  }

  document.startViewTransition(async () => {
    const response = await fetch(href);
    const html = await response.text();
    document.body.innerHTML = new DOMParser()
      .parseFromString(html, 'text/html')
      .body.innerHTML;
  });
}

SPA route changes

// React example
function navigate(path) {
  if (!document.startViewTransition) {
    setRoute(path);
    return;
  }

  document.startViewTransition(() => {
    setRoute(path);
  });
}

Customizing with CSS

Default pseudo-elements

The API creates pseudo-elements you can style:

/* The old content (fading out) */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}

/* The new content (fading in) */
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

Slide animation

::view-transition-old(root) {
  animation: slide-out 0.3s ease-out;
}

::view-transition-new(root) {
  animation: slide-in 0.3s ease-in;
}

@keyframes slide-out {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}

@keyframes slide-in {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

Named transitions

Give elements unique names for targeted animations:

.hero-image {
  view-transition-name: hero;
}

.page-title {
  view-transition-name: title;
}

Now animate them specifically:

::view-transition-old(hero) {
  animation: scale-down 0.4s ease-out;
}

::view-transition-new(hero) {
  animation: scale-up 0.4s ease-in;
}

::view-transition-old(title) {
  animation: fade-out 0.2s ease-out;
}

::view-transition-new(title) {
  animation: fade-in 0.2s ease-in 0.1s both;
}

Shared element transitions

When the same element exists on both pages:

/* Product card on list page */
.product-card {
  view-transition-name: product-123;
}

/* Same product on detail page */
.product-hero {
  view-transition-name: product-123;
}

The browser automatically animates the element from its old position to its new position.

JavaScript API details

The transition object

const transition = document.startViewTransition(() => {
  updateDOM();
});

// Wait for transition to complete
await transition.finished;

// Wait for new content to be captured
await transition.updateCallbackDone;

// Skip the animation
transition.skipTransition();

Transition lifecycle

const transition = document.startViewTransition(async () => {
  await fetchAndUpdate();
});

// Ready: old snapshot captured
await transition.ready;
console.log('Old state captured');

// updateCallbackDone: DOM updated
await transition.updateCallbackDone;
console.log('DOM updated');

// finished: animation complete
await transition.finished;
console.log('Transition complete');

Framework integration

React with Next.js

'use client';

import { useRouter } from 'next/navigation';

export function Link({ href, children }) {
  const router = useRouter();

  function handleClick(e: React.MouseEvent) {
    e.preventDefault();

    if (!document.startViewTransition) {
      router.push(href);
      return;
    }

    document.startViewTransition(() => {
      router.push(href);
    });
  }

  return (
    <a href={href} onClick={handleClick}>
      {children}
    </a>
  );
}

Vue

<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();

function navigate(path) {
  if (!document.startViewTransition) {
    router.push(path);
    return;
  }

  document.startViewTransition(() => {
    router.push(path);
  });
}
</script>

Direction-aware transitions

Apply different animations based on navigation direction:

let direction = 'forward';

function navigate(href, dir = 'forward') {
  direction = dir;
  document.documentElement.dataset.direction = dir;

  document.startViewTransition(() => {
    setRoute(href);
  });
}
[data-direction="forward"]::view-transition-old(root) {
  animation: slide-left-out 0.3s ease-out;
}

[data-direction="forward"]::view-transition-new(root) {
  animation: slide-left-in 0.3s ease-in;
}

[data-direction="back"]::view-transition-old(root) {
  animation: slide-right-out 0.3s ease-out;
}

[data-direction="back"]::view-transition-new(root) {
  animation: slide-right-in 0.3s ease-in;
}

Performance considerations

Practice Reason
Keep callbacks fast Long DOM updates delay animation start
Use CSS animations GPU accelerated, smooth
Limit named elements Each name creates capture work
Avoid layout thrashing Causes jank during capture

Accessibility

Respect reduced motion preferences:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Common patterns

Page header morphing

.page-header {
  view-transition-name: header;
}

::view-transition-old(header),
::view-transition-new(header) {
  animation: none;
  mix-blend-mode: normal;
}

List to detail

// On list page
function openDetail(id) {
  document.querySelector(`#item-${id}`).style.viewTransitionName = 'item';

  document.startViewTransition(() => {
    setRoute(`/items/${id}`);
  });
}
.detail-hero {
  view-transition-name: item;
}

The clicked item animates to become the detail hero.

Summary

The View Transitions API enables smooth animated transitions between DOM states with minimal code. Call document.startViewTransition() with a callback that updates the DOM. Customize animations with CSS pseudo-elements. Use named transitions for shared element animations. Feature detect and provide fallbacks for unsupported browsers. Respect reduced motion preferences for accessibility.

Frequently Asked Questions

The View Transitions API enables smooth animated transitions between DOM states or pages. It captures before and after states and animates between them with a default cross-fade or custom CSS animations.

Chrome 111+, Edge 111+, and Opera have full support. Safari has partial support. Firefox is implementing. Always feature detect before using.

Call document.startViewTransition(() => updateDOM()). The browser captures the current state, runs your callback to update the DOM, captures the new state, and animates between them.

Yes. Use CSS with ::view-transition-old and ::view-transition-new pseudo-elements. Apply custom keyframe animations, control duration, and style specific named elements differently.

Yes. Wrap route updates in startViewTransition for SPA navigation. Next.js App Router can integrate with the API for smooth page transitions. Check next/view-transitions for framework support.

Related Posts