ViewTransition: waitUntil() method

The waitUntil() method of the ViewTransition interface delays finishing the view transition and the destruction of the associated pseudo-element tree until a Promise passed into the method has resolved.

Syntax

js
waitUntil(promise)

Parameters

promise

A Promise that, when resolved, causes the view transition to finish and the associated pseudo-element tree to be destroyed. This can be any promise.

Return value

None (undefined).

Description

When a same-document view transition is started (typically via Document.startViewTransition()), the browser automatically constructs a pseudo-element tree to display and animate outgoing and inbound changes to the DOM. This tree is constructed when the view transition starts animating and is destroyed when the animations associated with all view transition pseudo-elements reach the finished state (finished is resolved).

This works fine in most cases. However, some advanced use cases benefit from the pseudo-tree persisting beyond the animation finish state. This can be achieved using the waitUntil() method, which is passed a Promise as an argument. Calling waitUntil() causes the pseudo-tree to persist until the promise is resolved. At this point, the finished promise also resolves.

Repeated calls to waitUntil() specifying multiple different promises will delay the finish state until all the given promises are resolved.

Use cases include:

  • Combining a view transition with scroll-driven animations. When the transition animation is controlled by a scroll progress or view progress timeline, the subtree should persist when the animations finish since scrolling back should be able to animate the pseudo-elements in reverse.
  • Combining a view transition with requestAnimationFrame(). When you're updating the state of your elements in requestAnimationFrame() callbacks, the view transition system does not know how long to persist the pseudo-element tree for, and will do it immediately as soon as the CSS animations are finished.
  • Any situation in which you want to delay the view transition finishing until an event has occurred. You might for example want to start the view transition on pointerdown and not finish it until pointerup.

Examples

Basic waitUntil() usage

This example demonstrates basic usage of the waitUntil() method to delay a view transition started by a button or key press from finishing until the button or key press ends.

HTML

We include a <div> element containing page content, which includes a <p> element and a <button> element that when pressed will change the displayed content. The paragraph includes an aria-live attribute so that DOM updates are announced to screenreader users.

html
<div class="page">
  <p class="content" aria-live="polite">Hello! This is the first page.</p>
  <button>Change page</button>
</div>

We also include a second <p> element to log status messages into:

html
<p class="log"></p>

CSS

First, we apply a view-transition-name of page to our <div> element so that we can target just this area with the view transition animations rather than the entire MDN page.

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

Next, we set an animation-delay on the ::view-transition-new() pseudo-element (note how we are specifying the page tree rather than the default root tree). This delays the new DOM content's default fade-in transition by 0.25 seconds, so it fades in slightly after the old DOM content fades out.

css
::view-transition-new(page) {
  animation-delay: 0.25s;
}

Now we set a custom animation-duration and opacity on the ::view-transition-old() and ::view-transition-new() element. This has the effect of making the default fade-out and fade-in animations last for 0.5 seconds, and sets the content opacity to 0.5 until the view transition is finished.

css
::view-transition-old(page),
::view-transition-new(page) {
  animation-duration: 0.5s;
  opacity: 0.5;
}

JavaScript

Our script starts by grabbing references to the content paragraph, button, and log paragraph.

js
const content = document.querySelector(".content");
const btn = document.querySelector("button");
const log = document.querySelector(".log");

Next, we set event listeners on the button so that on pointerdown/keypress, the btnHandler() custom function is run. We specify the keypress handler to only fire once, otherwise it will fire multiple times when a key is long pressed, which cycles between views constantly and isn't the behavior we want.

js
btn.addEventListener("pointerdown", btnHandler);
btn.addEventListener("keypress", btnHandler, {
  once: true,
});

The btnHandler() function invokes Document.startViewTransition() to start the view transition, first running a custom function called updatePage() that performs the DOM updates that will be animated. Next, we create a variable called resolveTransition and a new Promise called p. We set resolveTransition equal to the promise callback's resolve function, so that when resolveTransition() is called, as we do inside the subsequent pointerup and keyup event listeners, the promise is resolved. Note that we have to reapply the keypress handler each time the keyup handler fires, as it only fires once each time.

We run waitUntil(), passing it the promise p as an argument. This means that the view transition will persist until p is resolved on pointerup. To prove this, we use the ViewTransition.finished promise to run a showLog() function once the transition is finished, which will print a message into the log paragraph.

js
function btnHandler() {
  const transition = document.startViewTransition(() => {
    updatePage();

    let resolveTransition;

    const p = new Promise((resolve) => {
      resolveTransition = resolve;
    });

    window.addEventListener("pointerup", () => {
      resolveTransition();
    });

    window.addEventListener("keyup", () => {
      resolveTransition();
      btn.addEventListener("keypress", btnHandler, {
        once: true,
      });
    });

    transition.waitUntil(p);
    transition.finished.then(() => showLog());
  });
}

Next, we define the updatePage() function, which updates the page DOM. It toggles between setting the content paragraph textContent equal to the first and the second pageContent array elements.

js
function updatePage() {
  if (content.textContent === pageContent[0]) {
    content.textContent = pageContent[1];
  } else {
    content.textContent = pageContent[0];
  }
}

const pageContent = [
  "Hello! This is the first page.",
  "Well, this is the second page.",
];

Finally, we define the showLog() function — this sets the log paragraph's textContent to "View transition finished", waits for one second, then sets it back to an empty string.

js
function showLog() {
  log.textContent = "View transition finished";
  setTimeout(() => {
    log.textContent = "";
  }, 1000);
}

Result

Try long pressing on the button with your keyboard, mouse, or other pointing device — you'll see that the cross-fade transition animation occurs, but the content remains greyed out (due to the opacity: 0.5 set on the view transition pseudo-elements) until you end the long press. This is because the p promise referenced inside the waitUntil() call is not resolved, and therefore, the view transition is not finished, until the pointerup/keyup events are fired.

The "View transition finished" log message also doesn't appear until the view transition is finished, because the function that handles this is tied to the ViewTransition.finished promise.

Specifications

Specification
CSS View Transitions Module Level 2
# dom-viewtransition-waituntil

Browser compatibility

See also