Demo: Skip view transition in favor of default user-agent animation

View transitions landed in Safari 18. However, the default swipe animation for back/forward navigation cannot be disabled.

This causes a double animation. I uploaded a proof of concept at https://jch.github.io/turbo-cancel-view-transition/

My fix is to skip view transitions on restore visits initiated by a swipe gesture.

  1. Register for swipe navigation events, annotate html
  2. turbo:visit annotates action so I know when it’s a restore
  3. turbo:before-cache removes <meta name=‘view-transition’ … /> on the current page
  4. turbo:load clean up
// Safari swipe forward/back animation cannot be skipped, conflicting with view transition animation
//
// Disable view transition animation when navigating with touch for Turbo `restore` actions.
// Turbo `advance`, `none` actions use view transition animations.
//
// WICG Proposal default UA transitions: https://github.com/WICG/view-transitions/blob/main/default-ua-transitions.md
window.addEventListener('touchend', () => document.documentElement.setAttribute('data-jch-navigate-touch', true));
window.addEventListener('touchcancel', () => document.documentElement.setAttribute('data-jch-navigate-touch', true));
window.addEventListener('wheel', () => document.documentElement.setAttribute('data-jch-navigate-touch', true));  // Mac two finger swipe
document.addEventListener('turbo:visit', (event) => {
  document.documentElement.setAttribute('data-turbo-visit-action', event.detail.action);
});

// turbo:before-cache happens before call to startViewTransition
//  - removing <meta name="view-transition" .../> in old page makes prefersViewTransitions() return false
document.addEventListener('turbo:before-cache', (event) => {
  const action = document.documentElement.getAttribute('data-turbo-visit-action');
  const touch = document.documentElement.getAttribute('data-jch-navigate-touch');
  if (action === 'restore' && touch) {
    console.log('turbo:before-cache removing view-transition meta tag');
    document.documentElement.querySelector("meta[name='view-transition']").remove();
  }
});

document.addEventListener('turbo:load', () => {
  document.documentElement.removeAttribute('data-turbo-visit-action');
  document.documentElement.removeAttribute('data-jch-navigate-touch');
});

My first attempt was to customize the render with turbo:before-render. I ran into two issues. If there is a view-transition meta tag in the head, it would error because turbo will have already started a transition. If the meta tag is removed and the view transition is started manually, neither the old nor new page will have the meta tags, meaning custom CSS animations will not be applied.

// turbo:before-render happens after call to startViewTransition
//   - starting another transition raises an error b/c transition is in progress
//   - skipping <meta name="view-transition" .../> in the layout means new page will be missing meta, so multi-page transitions in CSS won't match
//
document.addEventListener('turbo:before-render', (event) => {
  if (!document.startViewTransition) { return; }

  const action = document.documentElement.getAttribute('data-turbo-visit-action');
  const touch = document.documentElement.getAttribute('data-jch-navigate-touch');
  if (action === 'restore' && touch) { return; }

  event.preventDefault();
  document.startViewTransition(() => {
    event.detail.resume();
  });
});
1 Like