I wasted a complete Saturday on this.
Not a lazy Saturday both, however a type of uncommon, carved-out, “I’m lastly going to construct that factor” Saturdays. I’d seen Jake Archibald’s demos. I’d watched the Chrome Dev Summit discuss. I knew cross-document view transitions had been actual, that you possibly can get these slick native-feeling web page transitions on plain previous multi-page websites with no single framework. No React. No Astro. No client-side router pretending your multi-page software (MPA) is single-page software (SPA). Simply HTML pages linking to different HTML pages, with the browser dealing with the animation between them. Hell sure.
So I began constructing. And nothing labored.
The primary tutorial I discovered had me dropping into my . Appeared easy sufficient. I added it to each pages, clicked my hyperlink, and… nothing. No transition. No error. Only a regular, prompt web page load prefer it was 2004. I opened DevTools, double-checked my syntax, restarted the server, tried Chrome Canary, cleared the cache. Nothing. I did what any self-respecting developer does at that time – I copied the code character by character from the weblog publish and pasted it in. Nonetheless nothing.
I spent two hours satisfied I used to be an fool.
Seems that tag syntax? Deprecated. Gone. Chrome shipped it, then changed it with a CSS-based opt-in, and half the web’s tutorials nonetheless present the previous means. These older weblog posts nonetheless rank effectively. They give the impression of being authoritative. And so they’re simply mistaken now. Not mistaken as a result of the authors had been unhealthy – mistaken as a result of the spec moved beneath everybody’s ft and no person went again to replace their posts.
The opposite half of the tutorials I discovered had been about same-document view transitions. SPA stuff. doc.startViewTransition() referred to as in JavaScript whenever you swap DOM content material your self, which is cool and helpful however a very completely different function whenever you truly sit right down to implement it. The API floor is completely different. The psychological mannequin is completely different. The gotchas are very completely different. And but, Google “view transitions tutorial” and good luck determining which taste you’re studying about till you’re three paragraphs deep.
So when you’re right here, I’m guessing you’ve been by some model of this. You tried the meta tag. It didn’t work. You tried the JavaScript API on an actual multi-page website and realized it solely fires inside a single doc. You possibly obtained one thing half-working in a demo nevertheless it fell aside the second you added actual content material — photos stretching bizarre, transitions hanging for seconds with no rationalization, or your CSS file turning into 200 strains of view-transition-name declarations as a result of you have got a grid of 40 product playing cards. You blamed your self. It wasn’t your fault. The documentation ecosystem round this function is a large number proper now, and the spec has been a transferring goal.
That is Half 1 of a two-part collection, and it’s the article I want existed on that Saturday. We’re going to cowl the precise present method to choose in with @view-transition in CSS (not the meta tag, not JavaScript), then dig into the 4-second timeout that may silently kill your transitions on sluggish pages and the best way to debug it, then repair the facet ratio warping that makes each image-heavy transition seem like a enjoyable home mirror, and at last get a correct deal with on the pagereveal and pageswap occasions that provide you with programmatic management over the entire lifecycle.
In Half 2, we’ll sort out the scaling downside – the best way to deal with view-transition-name throughout dozens or tons of of components with out your stylesheet turning into a catastrophe, the distinction between view-transition-name and view-transition-class, just-in-time naming patterns, and doing prefers-reduced-motion the correct means.
Cross-Doc View Transitions Collection
- The Gotchas No one Mentions (You might be right here!)
- Scaling View Transitions Throughout A whole bunch of Parts (Subsequent Monday!)
Seize espresso. Perhaps a refill. This one’s dense and I’m not going to waste your time, however there’s loads of floor right here and none of it’s apparent.
The Outdated Approach is Lifeless
/* THIS is the present opt-in - goes in your CSS */
@view-transition {
navigation: auto;
}
Right here’s the minimal setup. Two HTML recordsdata, one CSS rule on every. Word that, as of 2026, cross-document view transitions are supported in Chromium-based browsers and Safari 18.2+. Firefox assist is in progress as I’m penning this.
That’s it. Two HTML recordsdata. One CSS rule on every. Click on a hyperlink between them in a supporting browser (like trendy Chromium or Safari 18.2+) and also you get a easy cross-fade. No JavaScript. No meta tags. No construct step. The browser snapshots the previous web page, snapshots the brand new web page, and animates between them routinely.
Now, why did the spec transfer from a meta tag to a CSS at-rule? It wasn’t arbitrary.
The meta tag was a blunt instrument. It was on or off for the whole web page. You couldn’t say “allow transitions on desktop however not on cell the place the animations really feel janky on low-end {hardware}.” You couldn’t conditionally choose in primarily based on person preferences. It was simply… there, or not.
The CSS strategy opens all of that up:
/* Solely allow transitions if the person hasn't requested for diminished movement */
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
}
/* Solely allow on viewports extensive sufficient for the animation to really feel good */
@media (min-width: 768px) {
@view-transition {
navigation: auto;
}
}
That’s an actual improve. You get the identical conditional energy you have already got with each different CSS function. Media queries, @helps, no matter scoping logic you need — all of it simply works as a result of the opt-in lives the place your types reside.
There’s additionally a subtlety that issues: the CSS rule will be completely different on the previous web page versus the brand new web page. Each pages have to choose in for the transition to fireside. If Web page A has @view-transition { navigation: auto; } however Web page B doesn’t, you get no transition. That is truly helpful – it means your 404 web page or your login redirect can skip transitions with none JavaScript coordination.
Another factor price noting right here: navigation: auto solely kicks in for user-initiated, same-origin navigations. If the person clicks an everyday hyperlink or hits the browser’s Again button, you get a transition. However window.location.href = "https://css-tricks.com/someplace" set programmatically, or a cross-origin hyperlink, or a kind submission with a POST? No transition. The browser is deliberately conservative about when it fires, and actually that’s the correct name. You don’t desire a fancy cross-fade on a POST request that’s making a cost.
Look, when you’ve been following an outdated tutorial and your transitions simply silently don’t work, that is virtually actually why. The meta tag shipped in Chrome 111, obtained a couple of months of real-world use, after which the Chrome workforce deprecated it in favor of the CSS at-rule beginning round Chrome 126. No console warning. No error. The previous syntax simply quietly does nothing now. Actually, a deprecation warning in DevTools would’ve saved me (and doubtless you) loads of grief, however right here we’re.
Swap the meta tag for the CSS rule. That’s the 1st step. Every little thing else on this article builds on it.
Your Transition Will Randomly Die, and Right here’s Why
// Drop this in your pages to see what's truly occurring
window.addEventListener("pagereveal", (occasion) => {
if (!occasion.viewTransition) {
console.log(
"No view transition - web page did not choose in or browser skipped it",
);
return;
} // That is the one which'll save your sanity
occasion.viewTransition.completed
.then(() => console.log("Transition accomplished ✅"))
.catch((err) => {
// You may see "TimeoutError" right here and nowhere else
console.error("Transition killed:", err.identify, err.message);
});
});
Right here’s the factor no person places of their weblog publish: cross-document view transitions have a tough 4-second timeout. If the brand new web page doesn’t attain a state the browser considers “renderable” inside 4 seconds of the navigation beginning, the transition simply… dies. No animation. No cross-fade. The brand new web page snaps in like view transitions don’t exist. And except you’ve obtained that pagereveal listener wired up and your console open, you gained’t get any indication that something went mistaken.
4 seconds sounds beneficiant — till it isn’t.
Take into consideration what occurs on an actual website. Your web page masses. The HTML arrives, advantageous, that’s quick. However possibly you’ve obtained an enormous hero picture that’s render-blocking. Perhaps there’s a sluggish API name that your server waits on earlier than sending the response – a product web page hitting a list service, a dashboard ready on analytics information, something with server-side rendering that really does work earlier than responding. Perhaps you’re on an honest connection however the web page has three net fonts loading from Google Fonts with font-display: block. Any of those can push you previous that 4-second window, and the timeout doesn’t care why you’re sluggish. It simply cuts the transition.
The actually maddening half? It really works completely on localhost. Your dev server responds in 80ms. The transition is butter. You deploy to manufacturing, your server’s cold-starting a lambda or your CDN cache missed, and immediately customers get zero transitions on the primary click on. You possibly can’t reproduce it domestically. You begin questioning all the things.
// You can too catch this on the OLD web page utilizing `pageswap`
// Helpful for cleanup or logging which navigations fail
window.addEventListener("pageswap", (occasion) => {
if (occasion.viewTransition) {
occasion.viewTransition.completed.catch((err) => {
// Log it, ship it to your analytics, no matter
console.warn("Outgoing transition aborted:", err.identify);
});
}
});
So, what do you truly do about it?
Choice one: make your web page quicker. I do know, groundbreaking recommendation. However critically – in case your cross-document transition is dying, that’s a sign your web page load is genuinely sluggish. The timeout is appearing as a efficiency canary. Have a look at your Efficiency tab in DevTools, run a Lighthouse audit (which can not be excellent), work out what’s blocking first render. This isn’t view-transition-specific recommendation, however the timeout forces you to care about it.
Choice two is extra fascinating, and it’s the factor I want I’d identified about instantly.
This:
…tells the browser: “Don’t take into account this web page renderable till a component matching #hero is within the DOM.” That feels like it might make issues slower, and in a means it does – it delays first paint. However for view transitions, that’s precisely what you need: you’re telling the browser to carry the snapshot till the vital content material is definitely there, quite than snapping a screenshot of a half-loaded web page or, worse, timing out as a result of some picture within the footer remains to be downloading and blocking one thing.
It’s a trade-off. You’re selecting a barely delayed, however easy, transition over a quick, but-broken, one.
Actually, the 4-second restrict might be the correct name from the browser’s perspective. You don’t desire a person clicking a hyperlink and gazing a frozen web page for 10 seconds whereas the browser waits to do a flowery animation. In some unspecified time in the future, simply displaying the rattling web page is healthier than a fairly transition. However I want Chrome would floor the timeout extra visibly – a DevTools warning, a efficiency marker, one thing. Proper now it fails silently and that’s the entire downside.
Another factor price realizing: the timeout clock begins when navigation begins, not when the brand new web page’s HTML begins arriving. Community latency counts. The Time to First Byte (TTFB) Core Net Very important counts. In case your server takes 2 seconds to reply and your web page takes 2.5 seconds to render after that, you’re over the restrict regardless that neither half feels sluggish by itself.
A debugging tip that’s saved me greater than as soon as: Chrome’s DevTools has an Animations panel (it’s beneath “Extra instruments” when you don’t see it) that may truly seize view transitions in motion. You possibly can sluggish them right down to 10% velocity, replay them, and examine the pseudo-element tree mid-animation. It’s not apparent that it really works for view transitions, nevertheless it does. Between that and the pagereveal listener above, you may diagnose most timeout points fairly shortly.
Put that pagereveal listener in early. Watch your console throughout testing. You’ll thank your self later.
Why Your Photos Look Like Taffy
This one’s simpler to point out with a same-document demo first (since you may truly run it in a single file), however the issue and the repair are equivalent for cross-document transitions.
Run that. Click on the picture. Watch the canine flip into foolish putty.
The picture itself has object-fit: cowl on each side. The thumbnail appears advantageous, the hero appears advantageous. However through the transition? The browser doesn’t transition your aspect. It takes a screenshot of the previous state, takes a screenshot of the brand new state, and morphs between them. These screenshots are flat raster photos. Your rigorously utilized object-fit? Gone. The browser is simply scaling a bitmap from one field dimension to a different, and when a 150×150 sq. will get stretched right into a 600×300 rectangle, you get taffy.
Right here’s the repair:
/* THE FIX - goal the transition pseudo-elements straight */
::view-transition-old(hero-img),
::view-transition-new(hero-img) {
/* Deal with the snapshot like a picture in a container - crop, do not stretch */
object-fit: cowl;
overflow: hidden;
}
That’s the entire thing. Two properties on two pseudo-elements.
What’s truly occurring: the browser generates a tree of pseudo-elements for each named transition. For a component with view-transition-name: hero-img, you get this construction through the animation:
::view-transition
└── ::view-transition-group(hero-img)
├── ::view-transition-old(hero-img)
└── ::view-transition-new(hero-img)
The ::view-transition-group easily animates its width and peak from the previous dimensions to the brand new ones. That’s the morphing rectangle you see. Inside it, the previous and new pseudo-elements maintain the precise bitmap snapshots, and by default they’re set to object-fit: fill – which means “stretch to fill no matter field you’re in, facet ratio be damned.”
Switching to object-fit: cowl tells these snapshots to keep up their facet ratio and crop the overflow as an alternative. Similar psychological mannequin as a background picture with background-size: cowl. The transition nonetheless animates the field from sq. to rectangle (or no matter your shapes are), however the picture inside crops gracefully as an alternative of warping.
You might additionally use object-fit: comprise right here when you’d quite see the total picture with letterboxing as an alternative of cropping. It is dependent upon what appears proper in your content material. However cowl is what you’ll need 90% of the time, particularly for product photos and hero photographs.
For cross-document transitions, the CSS is equivalent – you simply put it in each pages’ stylesheets:
/* This works cross-document. Similar selectors, identical repair. */
/* Put it in your shared CSS file that each pages load. */
@view-transition {
navigation: auto;
}
::view-transition-old(hero-img),
::view-transition-new(hero-img) {
object-fit: cowl;
}
/* You can too management the animation timing on the group */
::view-transition-group(hero-img) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
Actually, I feel object-fit: cowl needs to be the default on these pseudo-elements as an alternative of fill. I get why the spec selected fill – it’s predictable, it matches what object-fit defaults to on changed components all over the place else in CSS – however in apply, how typically do you truly need a stretched bitmap throughout a transition? Nearly by no means. You’ll be including this override on principally each picture transition you construct.
Another variant that’s helpful when the facet ratios are wildly completely different – say a tall portrait thumbnail transitioning right into a cinematic widescreen hero:
/* Positive-tune the place the crop occurs on all sides of the transition */
::view-transition-group(hero-img) {
overflow: hidden;
border-radius: 8px; /* maintain it fairly mid-flight */
}
::view-transition-old(hero-img) {
object-fit: cowl;
object-position: heart heart;
}
::view-transition-new(hero-img) {
object-fit: cowl;
object-position: heart high; /* maintain the highest of the hero seen */
}
You possibly can set completely different object-position values on previous versus new, which helps you to management the place the crop occurs on all sides of the transition independently. The previous thumbnail may look greatest cropped from heart. The brand new hero may have to anchor to the highest. Combine and match.
This took me an embarrassingly very long time to determine. The repair is 2 strains of CSS, however when you don’t know the pseudo-element tree exists, you don’t even know what to focus on. Now you do.
The Two Occasions That Tie it All Collectively
You’ve already seen pagereveal and pageswap present up within the code above, however let’s take a step again and speak about what they really are. Understanding these two occasions goes to be vital, as a result of in Half 2 we’ll lean on them closely for the just-in-time naming sample that makes view transitions truly scale.
Cross-document view transitions occur throughout two pages that don’t have any JavaScript connection to one another. Web page A doesn’t find out about Web page B’s DOM. For the reason that previous and new pages don’t have any method to talk straight, these occasions are your solely method to coordinate the transition on each side. Web page B didn’t exist when Web page A was working. So how do you coordinate something? How do you resolve which components to call, or customise the transition primarily based on the place the person is heading?
That’s what these two occasions are for. They’re your hooks into the transition lifecycle, one on all sides of the navigation.
pageswap fires on the outgoing web page, proper earlier than it will get changed. That is your final probability to the touch the previous web page’s DOM earlier than the browser snapshots it. The occasion offers you two key properties:
occasion.viewTransition: the ViewTransition object for this navigation, ornullif no transition is going on.occasion.activation: a NavigationActivation object that tells you the place the person goes.
That activation property is the actually helpful one. occasion.activation.entry.url offers you the vacation spot URL, and occasion.activation.navigationType tells you whether or not it’s a push, change, traverse (again/ahead), or reload. This implies you may customise the outgoing aspect of the transition primarily based on the vacation spot. On a product itemizing web page, for instance, you may examine which product the person clicked, discover the matching card, and assign a view-transition-name to simply that aspect proper earlier than the snapshot occurs.
pagereveal fires on the incoming web page, proper after the web page turns into energetic however whereas the transition remains to be working. That is your probability to arrange the brand new aspect. The occasion offers you:
occasion.viewTransition: identical deal, the ViewTransition object ornull.
On the incoming web page, you examine the place the person got here from utilizing navigation.activation.from.url (by way of the Navigation API), and also you learn the present URL from window.location. Between these two items of data, you realize precisely what sort of navigation simply occurred and may arrange the incoming web page’s transition components accordingly.
Right here’s the total lifecycle so as:
- Person clicks a hyperlink on Web page A.
pageswapfires on Web page A. That is your window to call components and customise outgoing state.- Browser snapshots the previous web page (capturing any named components).
- Navigation occurs, new web page masses.
pagerevealfires on Web page B. You possibly can identify components, customise incoming state.- Browser snapshots the brand new web page.
- Transition animates between the 2 snapshots.
viewTransition.completedresolves (or rejects) on each side.
Three issues to remember with these occasions:
First, at all times guard with if (!occasion.viewTransition) return on the high of your handlers. pagereveal truly fires on each navigation – preliminary web page load, again/ahead, the works – not simply view transitions. If there’s no transition occurring, occasion.viewTransition will probably be null, and your handler ought to bail out gracefully. These handlers are transition sugar, not software logic. By no means put unwanted side effects in them that you simply want for the web page to work.
Second, pageswap solely fires if the previous web page opted into view transitions and the navigation is same-origin. If the person middle-clicks to open in a brand new tab, or the navigation goes cross-origin, the occasion both gained’t hearth or occasion.viewTransition will probably be null. That’s advantageous, your guard clause handles it.
Third, and that is straightforward to miss: each occasions provide you with entry to viewTransition.completed, which is a promise that resolves when the transition completes or rejects if one thing goes mistaken (like a timeout). At all times use this for cleanup, as in eradicating view-transition-name values you set dynamically, resetting state, no matter. Stale names from a earlier transition will wreck your subsequent one.
We’ve been utilizing these occasions frivolously to date – a pagereveal listener to catch timeouts, a pageswap listener for logging. In Half 2 of this little collection, they change into the spine of the entire scaling technique. Keep tuned.
What’s Subsequent
That covers the three gotchas that’ll chew you first: the deprecated meta tag that silently does nothing, the 4-second timeout that kills transitions with out telling you, and the picture distortion that turns each facet ratio change right into a enjoyable home mirror. Plus the 2 occasions that provide you with hooks into the entire lifecycle.
In Half 2, we’ll sort out the scaling downside. Once you’ve obtained a grid of 48 product playing cards and each wants a novel view-transition-name, how do you retain your CSS from exploding? The reply includes view-transition-class (which is completely different from view-transition-name in ways in which aren’t apparent), a just-in-time naming sample utilizing the pageswap and pagereveal occasions we simply lined. And one essential be aware: we’ll cowl prefers-reduced-motion in Half 2, however when you take nothing else from this collection, take this: animations can actually make individuals bodily nauseous. At all times examine that desire and respect it.
The gotchas are behind you. Now it’s time to make it scale.
Cross-Doc View Transitions Collection
- The Gotchas No one Mentions (You might be right here!)
- Scaling View Transitions Throughout A whole bunch of Parts (Subsequent Monday!)
