I wasted an entire Saturday on this. Not a lazy Saturday, either, but one of those rare carved-out days where you finally sit down to build that “one thing.” I’d seen the demos. I was sure cross-document view transitions were the future: slick, native-feeling page transitions on plain old multi-page sites (MPAs) without the overhead of React or Astro. No client-side router faking it, just the browser doing the animation. So I started building. And nothing worked.
The first tutorial I found had me drop a meta tag into my header. I refreshed, clicked, and… nothing. Just a normal, instant page load like it was 2004. I spent two hours convinced I was an idiot before I realized the spec had moved under my feet. If you’re stuck on this too, you’re not alone, because the documentation is a mess. For practical implementations, these View Transitions API recipes will give you a head start.
The meta tag is dead, long live CSS
Most tutorials still tell you to use a meta tag for cross-document view transitions. They’re wrong. Chrome shipped the meta tag, decided it was a blunt instrument, and replaced it with a CSS-based opt-in. The old syntax now quietly does nothing, with no console errors and no warnings. Swap that tag for a CSS at-rule.
/* DEPRECATED - Do not use this */
<meta name="view-transition" content="same-origin">
/* THE CURRENT WAY - Place this in your shared CSS */
@view-transition {
navigation: auto;
}
Why the change? CSS gives you control. You can wrap the opt-in in media queries, so you can skip transitions on low-end mobile devices or respect prefers-reduced-motion. Also, both pages have to opt in. If Page A has the rule and Page B doesn’t, the transition won’t fire.
Why your cross-document view transitions fail silently
This is the one that kills most projects: the 4-second timeout. If the new page doesn’t reach a “renderable” state within 4 seconds of the navigation starting, the browser drops the transition and the page just snaps in. On localhost, your 80ms response time makes it look like butter. In production, a cold-starting lambda or a slow CDN cache miss breaks it right away.
To debug it, hook into the pagereveal event. That’s the only way to catch the error the browser refuses to show in the DevTools console. I drop this snippet into my boilerplate every time to save my sanity.
window.addEventListener("pagereveal", (event) => {
if (!event.viewTransition) return;
event.viewTransition.finished
.then(() => console.log("Transition success!"))
.catch((err) => {
// This is where you see the "TimeoutError"
console.error("Transition failed:", err.name);
});
});
If you’re dealing with slow loads, you can put blocking="render" on critical elements. That tells the browser to hold the snapshot until the element is ready. You trade a slightly later start for a transition that actually runs smoothly. We covered similar performance strategies when looking at how core is fixing admin performance.
Fixing the aspect ratio warping
During a transition, the browser doesn’t animate the DOM element. It takes a raster screenshot of the old state and the new state and morphs between them. If your thumbnail is a 1:1 square and the hero is a 16:9 cinematic shot, the browser stretches that bitmap like taffy, and it looks amateur. The fix is to target the pseudo-elements directly and override the default object-fit: fill.
/* Target the transition pseudo-elements */
::view-transition-old(hero-img),
::view-transition-new(hero-img) {
/* Maintain aspect ratio and crop instead of stretching */
object-fit: cover;
overflow: hidden;
}
I really think object-fit: cover should be the browser default here. Until it is, you’ll be writing those two lines of CSS for nearly every image-heavy cross-document view transitions setup you build. They let the morph crop cleanly instead of distorting the pixels.
If cross-document view transitions work is eating your dev hours, I can take it on. I’ve been working with WordPress since the 4.x days, and I know how to get these newer APIs running without breaking a stable legacy site.
The lifecycle in short
You need to understand the pageswap and pagereveal events to scale this. pageswap fires on the outgoing page, and it’s your last chance to name elements. pagereveal fires on the incoming page, where you prepare the new state. In Part 2, we’ll use these events for “just-in-time” naming, so you can handle hundreds of elements without a 2,000-line stylesheet.
For more on the standard itself, see the MDN View Transition API documentation or the latest Chrome Dev Guide for MPAs.