Mastering Dynamic Page Transitions with the View Transition API

Web Development

Explore the View Transition API for animating elements between page navigations. Learn about default, custom, page-based, and connected element transitions with code examples.

Brought to you by: NERV

God's in his Heaven, all's right with the world


View Transition API

Published: November 2025

Status: Seed

Views: 3274

Target Audience: Front-end developers


The View Transition API offers a robust method to create animated transitions for elements when navigating between different pages. While it is supported across all major browsers, Firefox currently provides only limited functionality.

Default Transition

To enable the API, you must opt-in by adding the following CSS rule to each page:

/index.css
@view-transition {
  navigation: auto;
}

This CSS snippet activates the default cross-fade animation, which subtly decreases the opacity of outgoing elements while simultaneously increasing the opacity of incoming elements.

GO TO SITE (An arrow within a square pointing to the top-right)

Each example's CSS and JavaScript code is unminified and embedded directly within the HTML file.

Custom Transition

Beyond the default, you can customize the animation using standard CSS selectors and keyframes:

/index.css
@keyframes fade-in {
  from { opacity: 0; }
}
@keyframes fade-out {
  to { opacity: 0; }
}
@keyframes slide-to-left {
  to { translate: -120px; }
}
@keyframes slide-from-right {
  from { translate: 120px; }
}

/* Set the animation for outgoing page elements */
::view-transition-old(root) {
  animation: slide-to-left 1s, fade-out 1s;
}

/* Set the animation for incoming page elements */
::view-transition-new(root) {
  animation: slide-from-right 1s, fade-in 1s;
}

GO TO SITE (An arrow within a square pointing to the top-right)

Page-based Transition

It's also possible to vary the animation based on the destination page. The example below demonstrates sliding elements left when navigating to 'page A' and right when returning to the 'index' page. This approach leverages:

  • pageswap: An event triggered on the outgoing page just before the last frame is rendered.
  • pagereveal: An event fired on the incoming page after initialization but prior to its first render.
  • window.navigation: A property used to obtain URLs, though its availability is limited. A workaround involves storing the URL in sessionStorage on the outgoing page and retrieving it on the incoming page.
/index.css
:root {
  --transition-old: slide-to-left 1s, fade-out 1s;
  --transition-new: slide-from-right 1s, fade-in 1s;
  --transition-old-reverse: slide-to-right 1s, fade-out 1s;
  --transition-new-reverse: slide-from-left 1s, fade-in 1s;
}

::view-transition-old(root) {
  animation: var(--transition-old);
}

::view-transition-new(root) {
  animation: var(--transition-new);
}
/index.js
function isReverseTransition(toURL) {
  /* Using endsWith because I'm iframing the page */
  /* return toURL.pathname === "/index.html"; */
  return toURL.pathname.endsWith("/index.html");
}

async function setTemporaryReverseTransition(transitionPromise) {
  const root = document.documentElement;
  root.style.setProperty("--transition-old", "var(--transition-old-reverse)");
  root.style.setProperty("--transition-new", "var(--transition-new-reverse)");
  await transitionPromise;
  // Clean up
  root.style.removeProperty("--transition-old");
  root.style.removeProperty("--transition-new");
}

function onTransition(toURL, evt) {
  if (isReverseTransition(toURL)) {
    setTemporaryReverseTransition(evt.viewTransition.finished);
  }
}

window.addEventListener("pageswap", async (evt) => {
  // Not used: just demonstrating how to access
  const fromURL = new URL(evt.activation.from.url);
  const toURL = new URL(evt.activation.entry.url);
  onTransition(toURL, evt);
});

window.addEventListener("pagereveal", async (evt) => {
  const toURL = new URL(window.navigation.activation.entry.url);
  /* evt.viewTransition doesn't exist on page load (pagereveal will trigger on page load) */
  if (evt.viewTransition) {
    onTransition(toURL, evt);
  }
});

GO TO SITE (An arrow within a square pointing to the top-right)

WARNING: The pagereveal event listener must execute before the first rendering opportunity. Therefore, it needs to be registered within a classic parser-blocking script placed in the <head> of your HTML (not a module, async, or defer script). If you must use module or async, ensure it's marked as render-blocking by adding blocking=render.

Connect Elements

To create a morphing animation between elements on different pages, assign them the same view-transition-name property. This causes the outgoing element to smoothly transform into the incoming element.

/index.css
.greenSquare {
  width: 100px;
  aspect-ratio: 1;
  margin-right: 240px;
  background-color: green;
  view-transition-name: my-transition;
}

.redSquare {
  width: 200px;
  aspect-ratio: 1;
  margin-left: 240px;
  background-color: red;
  view-transition-name: my-transition;
}

GO TO SITE (An arrow within a square pointing to the top-right)

Multiple Transitions

You can orchestrate more than one animation within a single transition. For instance, you can:

  • Customize the default animation duration (here, to two seconds) for anchors.
  • Create a new, distinct transition (e.g., my-transition) specifically for squares.
/index.css
.greenSquare, .redSquare {
  /* ... other styles ... */
  view-transition-name: my-transition;
}

::view-transition-old(root) {
  animation-duration: 2s;
}

::view-transition-new(root) {
  animation-duration: 2s;
}

::view-transition-old(my-transition) {
  animation: slide-to-left 1s, fade-out 1s;
}

::view-transition-new(my-transition) {
  animation: slide-from-right 1s, fade-in 1s;
}

GO TO SITE (An arrow within a square pointing to the top-right)

UI Example: Menu

The View Transition API is exceptionally well-suited for animating transitions from a menu to a selected item's detail page. This often involves a small image in the menu expanding into a larger image on the item page. This effect can be achieved by:

  • Setting a view-transition-name on the image element for each item page.
  • Temporarily assigning the view-transition-name to a menu item's image:
    • When the menu item is clicked.
    • When navigating back to the menu page, using the pagereveal event.
/index.js
async function setTemporaryReverseTransition(elem, transitionPromise) {
  elem.style.viewTransitionName = "my-transition";
  // Cleanup
  await transitionPromise;
  elem.style.removeProperty("view-transition-name");
}

window.addEventListener("pagereveal", async (evt) => {
  const fromURL = new URL(navigation.activation.from.url);
  const { pathname } = fromURL;
  const parts = pathname.split("/");
  const anchor = document.querySelector(`a[href="${parts.pop()}"]`);

  // Required because pagereveal event triggers on initial page load
  if (!anchor) return;

  const sibling = anchor.nextElementSibling;
  setTemporaryReverseTransition(sibling, evt.viewTransition.finished);
});

// Add transition name to square when anchor clicked
document.addEventListener("DOMContentLoaded", () => {
  const anchors = document.querySelectorAll("a");
  anchors.forEach((anchor) => {
    anchor.addEventListener("click", (evt) => {
      // Capture the square
      const sibling = anchor.nextElementSibling;
      sibling.style.viewTransitionName = "my-transition";
    });
  });
});

GO TO SITE (An arrow within a square pointing to the top-right)

Overflow: hidden - A Known Issue

One challenge with the View Transition API is that elements contained within a parent with overflow: hidden may visually break out of their container during a transition. As illustrated below, squares within a circular div with overflow: hidden will unexpectedly appear outside the circle during the animation.

GO TO SITE (An arrow within a square pointing to the top-right)

References


Feedback

Have any feedback about this note or just want to comment on the state of the economy? Send Feedback


=640&q=75)