Skip to main content

CSS View Transitions API Guide

Create smooth, native-feeling page transitions with the View Transitions API.
Control animations entirely with CSS.

1. Introduction to View Transitions

The View Transitions API provides a mechanism for creating animated transitions between different DOM states. It captures snapshots of the page before and after a change, then animates between them.

What are View Transitions?

  • A native browser API for animating DOM changes
  • Creates smooth transitions without complex animation libraries
  • Works with both SPAs (single-page apps) and MPAs (multi-page apps)
  • Animations are fully controllable with CSS

Browser Support

Chrome
111+
Edge
111+
Safari
18+
Firefox
Partial
Opera
97+

Note: View Transitions are progressively enhanced. If the browser doesn't support them, the DOM update still happens - just without the animation.

Same-Document vs Cross-Document Transitions

Same-Document (SPA)

For single-page applications where navigation happens via JavaScript. Use document.startViewTransition().

Cross-Document (MPA)

For traditional multi-page applications with full page navigations. Use @view-transition CSS at-rule.

2. Basic Usage

The simplest way to use View Transitions is with the document.startViewTransition() API. This captures the current state, runs your update callback, then animates to the new state.

The startViewTransition() API

// Basic usage
document.startViewTransition(() => {
  // Update the DOM here
  updateDOM();
});

// With async/await
document.startViewTransition(async () => {
  const data = await fetchNewContent();
  container.innerHTML = data;
});

Default Crossfade Animation

Without any custom CSS, the View Transitions API applies a default crossfade animation between the old and new states.

// This will create a smooth crossfade by default
document.startViewTransition(() => {
  document.querySelector('.content').textContent = 'New content!';
});

Handling the Transition Object

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

// Wait for the transition to finish
await transition.finished;

// Wait for the new view to be ready
await transition.ready;

// Skip the transition animation
transition.skipTransition();

Important: The callback passed to startViewTransition()should update the DOM synchronously or return a Promise. The API waits for the Promise to resolve before capturing the new state.

3. Customizing Transitions with CSS

The real power of View Transitions comes from CSS customization. The API creates pseudo-elements that you can style with standard CSS animations.

View Transition Pseudo-Elements

::view-transition - Root container for all transitions
::view-transition-group(name) - Animates size and position
::view-transition-image-pair(name) - Contains old and new images
::view-transition-old(name) - Screenshot of the old state
::view-transition-new(name) - Live view of the new state

Customizing the Root Transition

/* Change animation duration */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.5s;
}

/* Disable the default crossfade */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

Custom Fade Animation

/* Custom fade-in animation */
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

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

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

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

Slide Transition

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

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

::view-transition-old(root) {
  animation: slide-out-left 0.4s ease-in-out forwards;
}

::view-transition-new(root) {
  animation: slide-in-right 0.4s ease-in-out forwards;
}

Scale and Fade

@keyframes scale-down-fade {
  to {
    opacity: 0;
    transform: scale(0.9);
  }
}

@keyframes scale-up-fade {
  from {
    opacity: 0;
    transform: scale(1.1);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

::view-transition-old(root) {
  animation: scale-down-fade 0.3s ease-out forwards;
}

::view-transition-new(root) {
  animation: scale-up-fade 0.3s ease-out forwards;
}

4. Named View Transitions

Named view transitions allow you to animate specific elements independently, creating more sophisticated transition effects like hero animations.

The view-transition-name Property

/* Give an element a unique transition name */
.hero-image {
  view-transition-name: hero;
}

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

/* Each name must be unique on the page */

Rule: Only one element can have a given view-transition-name at any time. If multiple elements share the same name, the transition will fail.

Hero Image Transition Example

A classic example is animating a thumbnail to a full-size image when navigating to a detail page.

/* On the list page */
.product-thumbnail {
  view-transition-name: product-image;
}

/* On the detail page */
.product-hero {
  view-transition-name: product-image;
}

/* The API will automatically animate between them! */

Styling Named Transitions

/* Style the hero image transition specifically */
::view-transition-group(hero) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Keep the image aspect ratio during animation */
::view-transition-old(hero),
::view-transition-new(hero) {
  object-fit: cover;
  width: 100%;
  height: 100%;
}

/* Different animations for different elements */
::view-transition-group(title) {
  animation-duration: 0.3s;
}

Disabling Transitions for Specific Elements

/* Exclude an element from the transition */
.no-transition {
  view-transition-name: none;
}

/* Or use the contain property */
.static-sidebar {
  view-transition-name: sidebar;
  contain: layout;
}

5. Practical Examples

Page Transition Effect

A smooth page transition for SPA navigation with a vertical slide effect.

// JavaScript: Trigger transition on navigation
async function navigateTo(url) {
  if (!document.startViewTransition) {
    // Fallback for unsupported browsers
    window.location.href = url;
    return;
  }

  const transition = document.startViewTransition(async () => {
    const response = await fetch(url);
    const html = await response.text();
    document.querySelector('main').innerHTML = html;
  });

  await transition.finished;
}
/* CSS: Page transition styles */
@keyframes slide-up-out {
  to {
    opacity: 0;
    transform: translateY(-30px);
  }
}

@keyframes slide-up-in {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
}

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

::view-transition-new(root) {
  animation: slide-up-in 0.25s ease-out forwards;
}

List Item Reordering

Smoothly animate list items when they are reordered.

/* Each list item gets a unique name */
.list-item {
  view-transition-name: var(--item-name);
}

/* Use CSS custom properties for unique names */
.list-item:nth-child(1) { --item-name: item-1; view-transition-name: item-1; }
.list-item:nth-child(2) { --item-name: item-2; view-transition-name: item-2; }
.list-item:nth-child(3) { --item-name: item-3; view-transition-name: item-3; }
/* ... and so on */

/* Animate the group (position and size) */
::view-transition-group(item-1),
::view-transition-group(item-2),
::view-transition-group(item-3) {
  animation-duration: 0.3s;
  animation-timing-function: ease-out;
}
// JavaScript: Reorder with transition
function moveItemUp(index) {
  document.startViewTransition(() => {
    const list = document.querySelector('.list');
    const items = Array.from(list.children);

    if (index > 0) {
      list.insertBefore(items[index], items[index - 1]);
    }
  });
}

Image Gallery Transitions

Click a thumbnail to expand it into a full-screen view with a smooth morph effect.

/* Thumbnail grid */
.gallery-thumb {
  view-transition-name: gallery-image;
  cursor: pointer;
}

/* Full-size view */
.gallery-full {
  view-transition-name: gallery-image;
  position: fixed;
  inset: 0;
  object-fit: contain;
  background: rgba(0, 0, 0, 0.9);
}

/* Customize the morph animation */
::view-transition-group(gallery-image) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Keep the background from animating */
.gallery-backdrop {
  view-transition-name: none;
}

Tab Switching Animation

Animate between tab panels with directional awareness.

// JavaScript: Tab switching with direction detection
let currentIndex = 0;

function switchTab(newIndex) {
  const direction = newIndex > currentIndex ? 'forward' : 'backward';
  document.documentElement.dataset.direction = direction;

  document.startViewTransition(() => {
    // Hide current panel, show new panel
    document.querySelectorAll('.tab-panel').forEach((panel, i) => {
      panel.hidden = i !== newIndex;
    });
    currentIndex = newIndex;
  });
}
/* CSS: Direction-aware animations */
.tab-panel {
  view-transition-name: tab-content;
}

/* Forward navigation (left to right) */
[data-direction="forward"]::view-transition-old(tab-content) {
  animation: slide-out-left 0.3s ease-out;
}

[data-direction="forward"]::view-transition-new(tab-content) {
  animation: slide-in-right 0.3s ease-out;
}

/* Backward navigation (right to left) */
[data-direction="backward"]::view-transition-old(tab-content) {
  animation: slide-out-right 0.3s ease-out;
}

[data-direction="backward"]::view-transition-new(tab-content) {
  animation: slide-in-left 0.3s ease-out;
}

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

6. Cross-Document Transitions (MPA)

For traditional multi-page applications, you can enable view transitions between full page navigations using CSS alone - no JavaScript required!

The @view-transition At-Rule

/* Enable cross-document view transitions */
@view-transition {
  navigation: auto;
}

/* This goes in your CSS and applies to all same-origin navigations */

Requirements: Cross-document transitions only work for same-origin navigations (same domain). They won't work for external links or cross-origin redirects.

Styling MPA Transitions

/* Enable transitions */
@view-transition {
  navigation: auto;
}

/* Customize the page transition */
::view-transition-old(root) {
  animation: fade-scale-out 0.4s ease-out forwards;
}

::view-transition-new(root) {
  animation: fade-scale-in 0.4s ease-out forwards;
}

@keyframes fade-scale-out {
  to {
    opacity: 0;
    transform: scale(0.95);
  }
}

@keyframes fade-scale-in {
  from {
    opacity: 0;
    transform: scale(1.05);
  }
}

Shared Elements Across Pages

The same view-transition-name technique works across pages. If an element on page A and an element on page B share the same name, they will animate between each other.

/* page-list.html */
.product-card img {
  view-transition-name: product-hero;
}

/* page-detail.html */
.product-detail img {
  view-transition-name: product-hero;
}

/* The image will smoothly morph from card to detail view */

Progressive Enhancement

/* Feature detection with @supports */
@supports (view-transition-name: test) {
  @view-transition {
    navigation: auto;
  }

  .animate-on-transition {
    view-transition-name: my-element;
  }
}

/* Browsers without support simply skip these rules */

Best Practices

  • Keep transition durations short (200-500ms) for perceived performance
  • Use prefers-reduced-motion media query to respect user preferences
  • Test transitions on slower devices to ensure smooth performance
  • Provide fallbacks for browsers that don't support View Transitions
  • Avoid animating too many elements at once to prevent jank

Respecting Reduced Motion

/* Disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }

  /* Or reduce duration significantly */
  ::view-transition-group(*) {
    animation-duration: 0.01s;
  }
}

Quick Reference

CSS Properties

view-transition-name: <name>

CSS At-Rules

@view-transition { navigation: auto; }

Pseudo-Elements

::view-transition
::view-transition-group(name)
::view-transition-image-pair(name)
::view-transition-old(name)
::view-transition-new(name)

JavaScript API

document.startViewTransition(callback)
transition.ready
transition.finished
transition.skipTransition()