Skip to main content

Cancel in-progress view transitions on user interaction

Make sure that view transitions don't block user interactions by canceling them when users attempt to interact during the transition period, preventing INP delays.


The View Transitions API creates smooth, app-like transitions between pages. However, cross-document view transitions block rendering between the initial page screenshot and the server response. When users interact during this blocked rendering period, it creates large INP delays.

Performance profiling has shown that view transitions can significantly delay user interactions. The blocked rendering period prevents the browser from responding to user input, creating poor INP scores, especially when users click or tap during navigation transitions.

Chrome's performance best practices recommend canceling view transitions when users interact with the page to prevent these delays. Transitions improve perceived performance with an app-like experience and smooth visual continuity, but they can hurt measured performance (INP) if not implemented carefully.


Anchor to Opt in to cross-document transitions with ,[object Object]Opt in to cross-document transitions with @view-transition

Cross-document view transitions don't happen unless both documents opt in with an @view-transition at-rule. Add it to your theme's stylesheet, which every page loads:

assets/base.css

@view-transition {
navigation: auto;
}

The opt-in is checked per document: the outgoing document's rule gates pageswap, and the incoming document's rule gates pagereveal. Without it, event.viewTransition is always null and the cancellation code in the next section never runs.

Anchor to Cancel transitions on user interactionCancel transitions on user interaction

Add event listeners that cancel the view transition when users interact:

function cancelOnInteraction(event) {
const viewTransition = event.viewTransition;
if (!viewTransition) return;

["pointerdown", "keydown"].forEach((eventType) => {
document.addEventListener(
eventType,
() => {
viewTransition.skipTransition();
},
{ once: true }
);
});
}

// Outgoing page: covers the blocked rendering period.
window.addEventListener("pageswap", cancelOnInteraction);

// Incoming page: covers the transition animation itself.
window.addEventListener("pagereveal", cancelOnInteraction);
Caution

pageswap and pagereveal fire on window, not on the Navigation API's navigation object. Registering them anywhere else silently does nothing, so the transition is never canceled.

Use both pointerdown, which captures mouse clicks and touch taps, and keydown, which captures keyboard interactions. Handle pagereveal as well as pageswap: pageswap fires on the outgoing document, but the transition animates on the incoming one.

The { once: true } option makes sure that listeners fire only once per transition, provides automatic cleanup without a manual removeEventListener, and prevents memory leaks.

Anchor to Use view transitions selectivelyUse view transitions selectively

Apply view transitions strategically, not universally.

Good use cases:

  • Product card to product page navigation.
  • Image gallery browsing.
  • Variant image switching.
  • Collection to product navigation.

Avoid them for:

  • Every navigation. Too aggressive.
  • Form submissions.
  • Checkout flows.
  • Admin and account pages.
  • External links.

Anchor to Respect user motion preferencesRespect user motion preferences

Always check for the reduced motion preference:

function userPrefersReducedMotion() {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

if (document.startViewTransition && !userPrefersReducedMotion()) {
// Apply view transition
}

Anchor to Provide a fallback for unsupported browsersProvide a fallback for unsupported browsers

Support differs between the two kinds of view transition:

Transition typeChrome and EdgeSafariFirefox
Cross-document (@view-transition, pageswap, pagereveal)126 or later18.2 or laterNot supported
Same-document (document.startViewTransition())111 or later18 or later144 or later

Always provide a fallback:

if (document.startViewTransition) {
// Use view transition
} else {
// Standard navigation
window.location = href;
}

Anchor to Define CSS animationsDefine CSS animations

::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
}

@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.01s;
}
}

Anchor to Full implementation with cancellationFull implementation with cancellation

class ViewTransitionManager {
constructor() {
const handle = (event) => this.handleViewTransition(event);

window.addEventListener("pageswap", handle);
window.addEventListener("pagereveal", handle);
}

handleViewTransition(event) {
const viewTransition = event.viewTransition;
if (!viewTransition) return;

const cancelTransition = () => {
viewTransition.skipTransition();
};

document.addEventListener("pointerdown", cancelTransition, { once: true });
document.addEventListener("keydown", cancelTransition, { once: true });

viewTransition.finished.finally(() => {
document.removeEventListener("pointerdown", cancelTransition);
document.removeEventListener("keydown", cancelTransition);
});
}
}

new ViewTransitionManager();

Anchor to Conditional transitions based on contextConditional transitions based on context

Don't try to drive a navigation from document.startViewTransition(). That method is same-document only: passing it a navigation callback produces a roughly 250 ms non-interactive cross-fade of the unchanged outgoing page, followed by an ordinary navigation with no transition at all.

To limit cross-document transitions to specific navigations, keep the @view-transition opt-in and skip the transition in pageswap when the destination isn't one you want to animate:

window.addEventListener("pageswap", (event) => {
const viewTransition = event.viewTransition;
if (!viewTransition) return;

const destination = event.activation?.entry?.url;
const isProductNavigation =
destination && new URL(destination).pathname.startsWith("/products/");

if (!isProductNavigation || userPrefersReducedMotion()) {
viewTransition.skipTransition();
}
});
Caution

Don't add a .catch() on viewTransition.finished as a navigation fallback. finished fulfills even when the transition is skipped, so the fallback is dead code. Only viewTransition.ready rejects on a skipped transition.

Anchor to CSS for view transitionsCSS for view transitions

::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
}

@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.01s;
}
}

::view-transition-old(product-image),
::view-transition-new(product-image) {
animation-duration: 0.25s;
}

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

Anchor to Common issue: view transitions blocking interactionsCommon issue: view transitions blocking interactions

Problem: The transition blocks the user from interacting until it completes. Solution: Implement cancellation on interaction, as shown in the Cancel transitions on user interaction section.

Anchor to Common issue: transitions on every navigationCommon issue: transitions on every navigation

Problem: Overuse causes motion sickness and slows navigation. Solution: Use view transitions selectively, only for product cards and galleries.

Anchor to Common issue: ignoring the reduced motion preferenceCommon issue: ignoring the reduced motion preference

Problem: Transitions show for users who prefer less motion. Solution: Check the prefers-reduced-motion media query.


  • Enable view transitions and profile navigation interactions in the Chrome DevTools Performance panel.
  • Interact during a transition (click, tap, or keyboard) and measure INP before and after adding the cancellation implementation.
  • Test navigation from a collection page to a product page during a transition.
  • Test multiple quick clicks during a transition.
  • Test keyboard navigation and touch interactions on mobile during a transition.
  • Test with a slow network using network throttling.
  • Compare with and without view transitions enabled.
  • Log transitions for debugging:
window.addEventListener("pageswap", (event) => {
if (event.viewTransition) {
console.log("View transition active");
event.viewTransition.finished.then(() => {
console.log("Transition completed");
});
}
});


Was this page helpful?