View Transitions API

View Transitions give you an easier way, by allowing you to make your DOM change without any overlap between states, but create a transition animation between the states using snapshotted views.[2]

Here's some minimal starter JS:

function navigate(url) {
  const update = () => { /* fetch HTML then update DOM */ };
  return 'startViewTransition' in document
    ? document.startViewTransition(update).finished
    : // Fallback for non-supporting browsers
      update();
}

This adds a default fade animation, and can be customised using CSS:

::view-transition-old(root) {
  animation: fade-out 100ms ease-out both;
}
::view-transition-new(root) {
  animation: fade-in 300ms 100ms ease-out both;
}

You can also scope the transitions to a particular element using the view-transition-name property:

body {
  view-transition-name: content;
}
::view-transition-group(content) {
  --out-time: 100ms;
}
::view-transition-old(content) {
  animation: fade-out var(--out-time) ease-out both;
}
::view-transition-new(content) {
  animation: fade-in 300ms var(--out-time) ease-out both;
}

Also, by default, if a scope exists in both of the old and new pages, the browser will attempt to transition the two elements, no extra code needed!