Designing view transitions

Last updated

For a while this site was using Motion and a FrozenRouter workaround to fade pages in and out on navigation. It worked, but it was a lot of plumbing having to wrap the route in AnimatePresence, freeze the router context so the outgoing tree wouldn’t unmount mid-animation, and keying everything off the pathname.

Now React supports the View Transitions API, which lets the browser snapshot the DOM before and after a navigation and animate between them natively. No frozen contexts, no presence tracking, no extra dependencies.

If you’re using Next.js, you need to enable the flag in next.config.js:

experimental: {
  viewTransition: true,
}

The whole layout collapses to this:

import { ViewTransition } from "react";

export default function Layout({ children }) {
  return (
    <ViewTransition name="page">
      <main>
        {children}
      </main>
    </ViewTransition>
  );
}

The name prop scopes the transition to <main> preventing the header to animate as well. The matching CSS does two things: it disables the default root crossfade, and it sequences the page out and in inside a single 400ms keyframe so the new page doesn’t overlap the old one.

@keyframes page-out {
  0% { opacity: 1; transform: translateY(0); }
  50%, 100% { opacity: 0; transform: translateY(-4px); }
}

@keyframes page-in {
  0%, 50% { opacity: 0; transform: translateY(4px); }
  100% { opacity: 1; transform: translateY(0); }
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
}

::view-transition-old(page) {
  animation: page-out 400ms ease both;
}

::view-transition-new(page) {
  animation: page-in 400ms ease both;
}

The same effect, with a fraction of the JavaScript and one less dependency needed.

Keep in mind that this is not supported in all browsers (yet), but for those cases it falls back to no animation, which is an acceptable degradation.