css.
css10 min read

css-cross-document-view-transitions-gotchas

css-cross-document-view-transitions-gotchas

Cross-Document View Transitions: A Pragmatic Intro to the Gotchas

A few months ago I tagged a hero image on a product detail page, then clicked through from the index in Chrome. It slid into place like it had always belonged there. Five minutes later I opened the same page in Safari and saw plain old white flash. Same code, same tag, no animation. That's the floor and ceiling of cross-document view transitions in one breath: gracefully invisible where they aren't supported, quietly cinematic where they are. This piece is the briefing I wish I'd had before I started tagging anything — moving parts first, the gotchas that bit me second.

What "Cross-Document" Actually Means

Cross-document gets the headlines, but same-document view transitions shipped first and quietly set every rule that followed. You trigger them from JavaScript via document.startViewTransition(updateCallback), mutate the DOM synchronously inside that callback, and the browser animates between the snapshot it took before your callback ran and the new state it sees after. The current document never goes away. That's exactly the model SPAs already use.

Cross-document transitions work differently. The user clicks a link, the browser unloads the current document, parses and loads the destination document, runs its scripts, fires its pageswap event, and then animates between the two pages as if they were a single timeline. The visual continuity is real even though two completely separate documents are involved. The opt-in lives in CSS, not JS: both pages need a @view-transition rule that explicitly allows the navigation, and the navigation has to be a same-origin, full-page navigation initiated by user action. That last clause hides a surprising number of gotchas — which is why this article exists.

The canonical reference is the W3C CSS View Transitions Module Level 2 draft, which defines the at-rule, the cross-document algorithm, and the JavaScript events. You can read it on the W3C CSS View Transitions Level 2 draft. The MDN reference page on @view-transition at MDN Web Docs is the friendlier entry point if you just want syntax and a working demo.

The Opt-In Looks Tiny But Carries a Lot of Implicit Rules

How much CSS does it take to opt a whole site into cross-document transitions? Two lines per page, in both documents:

@view-transition {
  navigation: auto;
}

That tiny block declares the document is ready to participate in a cross-document transition for any normal same-origin navigation. There are exactly two valid values for navigation today: auto and none. There's no same-origin-only knob because same-origin is already the default and only mode; cross-origin transitions are forbidden by design. There's no granular opt-in by URL pattern, no way to allow only push navigations and not replace navigations, and no way to negotiate animation specifics between the two documents through this rule alone. Everything beyond the at-rule lives in one of three places: ::view-transition-* pseudo-elements, view-transition-name declarations on real elements, or the JavaScript events the browser fires on the way through.

Treat that minimalism as a feature, not a bug. The web platform already knows what a same-origin navigation is, and the View Transitions API piggybacks on that existing definition rather than reinventing it.

Gotcha 1: Browser Support Is Still a Single-Vendor Story

A colleague spent a week polishing a navigation transition that her QA team couldn't see — they were on Safari, she was on Chrome, and as of mid-2026 only Chromium-based browsers ship cross-document view transitions in stable. Firefox has the API under a flag for same-document transitions and is still implementing the cross-document algorithm. Safari shipped same-document transitions in 18 but hasn't enabled cross-document mode in a stable release. That means anything you build today has to assume that for a non-trivial fraction of your audience, the navigation is a plain old hard page load with no transition at all.

The good news: the API is designed to fall back invisibly. If a browser doesn't understand @view-transition, it ignores the at-rule and ships the navigation as if you never opted in. If a browser understands the at-rule but can't match a transition group between the two documents, it falls back to the default cross-fade. You don't get a broken page; you just get the old behaviour. The bad news: your engineering team sees the transition every day in their Chromium-based dev environment and then ships a feature that two-thirds of your real users never experience. Plan for that mentally before you start tagging elements.

Gotcha 2: Same-Origin Means Same-Origin, Not Same-Site

The cross-document algorithm refuses to animate between two documents that don't share an origin. Two URLs share an origin when their scheme, host, and port all match exactly. A navigation from https://www.example.com to https://shop.example.com is cross-origin even though both are subdomains of example.com, and it'll silently downgrade to a plain navigation with no transition. This is a security and privacy decision: animating between origins could leak rendering information across security boundaries.

If your marketing site lives on www.example.com and your product application lives on app.example.com, you can't use cross-document transitions to bridge the two. Either consolidate origins or accept that the boundary between them is a hard cut. There's no opt-in flag, no CORS header, no postMessage handshake that overrides this rule.

Gotcha 3: Initial Navigation, Form Posts, and Bfcache Each Behave Differently

A user landing on your site through a fresh tab doesn't participate in a transition — there's no previous document to morph from. Obvious in hindsight, but it trips up demos that assume every page load is an animated transition. Server-rendered pages paint normally on the first visit and only animate on subsequent same-origin clicks.

Form submissions that end in POST-redirect-GET can participate in transitions in Chromium, but the snapshot taken on the way out is the form page, not the redirect target. If your POST handler returns HTTP 303 to a fresh page, the visual continuity might look strange because the elements you tagged on the form don't exist on the destination. Plan transitions around GET navigations first and treat form transitions as an enhancement.

The back-forward cache (bfcache) and view transitions interact carefully. When the browser restores a page from bfcache, no transition runs because the page never actually navigated in the visual sense. Hitting "back" from a bfcache-eligible page returns instantly, with no animation. That's generally what you want — but it does mean your "back" gesture and your forward "click" gesture animate inconsistently. Designing transitions that work in only one direction is the right default.

Gotcha 4: view-transition-name Must Be Unique Per Frame

The most common runtime error in any view transition setup is duplicate view-transition-name values inside a single document. If two visible elements both carry view-transition-name: hero-image, the browser aborts the transition and runs a non-animated navigation instead. You'll see a warning in DevTools. This is the right behaviour — the transition graph would otherwise be ambiguous.

The trap is that list pages often render many cards with the same template. If the template hard-codes view-transition-name: card-thumbnail, every card collides as soon as more than one renders. The fix is to make the name unique per item, usually by interpolating the item ID into the value via inline style or a CSS custom property. Modern build pipelines that ship server components handle this naturally; older Jamstack pipelines might need a small templating change.

Gotcha 5: Snapshots Are Bitmaps, Not Live DOM

Every tagged element becomes a ::view-transition-group containing ::view-transition-image-pair pseudo-elements during the animation. The "before" and "after" images are real raster snapshots of the source and destination elements. They're not the live DOM. That means:

  • Mid-transition you can't click, scroll, or otherwise interact with the snapshot. The user sees a movie, not a page.
  • Video and audio elements freeze on their last painted frame during the transition.
  • Iframes are paused. If your design depends on a live embed staying live across navigation, this is the wrong tool.
  • Position-sticky and fixed elements are captured at the position they had during the snapshot. Repositioning during the transition shows up as a jump from where the snapshot says they were.

Treat the transition window as visual punctuation, not an interactive moment. Keep it short. Many production sites cap their navigation transition at around 250 milliseconds and time the longest animation curve carefully.

Gotcha 6: Scroll Position Is Not What You Expect

When the browser snapshots the outgoing document, it captures only what was actually painted — generally the viewport plus a small overscroll buffer. The destination document loads with the scroll position the browser thinks it should have, which for a same-origin navigation is normally the top of the page. If you tagged an element that was off-screen at the moment of the click, the transition can't animate from a position the source frame doesn't know about, and you'll see either a fade or a jump.

The pragmatic workaround is to choose which elements to tag based on what's likely to be on screen at navigation time. A hero image at the top of an article page is a safe tag. A footer signature visible only after scroll isn't.

Gotcha 7: The Pageswap and Pagereveal Events Run Late

Cross-document transitions expose two new lifecycle events: pageswap on the outgoing document and pagereveal on the incoming document. Both fire after script has started executing on their respective pages, and both fire synchronously enough that you can call event.viewTransition.skipTransition() to cancel an animation that would have been awkward. If you need to bail out of a transition based on app state, this is the hook.

The gotcha is that any heavy work you do inside these handlers blocks the animation timeline. A pagereveal handler that synchronously parses a megabyte of JSON delays the start of the transition by exactly that long, and the user stares at a frozen "before" snapshot for a beat that has nothing to do with the animation itself. Defer non-essential work to requestIdleCallback and keep the handler under a frame.

Gotcha 8: Reduced-Motion Settings Override Your Design

The browser respects prefers-reduced-motion: reduce automatically for the default cross-fade, but custom transitions you author inside ::view-transition-group(*) selectors have to be guarded explicitly with the media query — otherwise users who asked the system for less motion get exactly the swooping animations they opted out of. This is the same accessibility hygiene you should already practice for ordinary CSS animations; it's doubly important here because the entire viewport is the animated surface.

Gotcha 9: Stylesheet and Asset Timing Can Cause "Flash of Untransitioned Content"

The browser can't start the transition until the destination document is ready enough to be snapshotted. If your destination page ships several render-blocking stylesheets or critical assets behind slow CDN edges, the user clicks the link and stares at the source page for the entire network round trip before any animation begins. The browser does suppress the new document's paint until it has enough to snapshot, but it can't magically speed up its load. Spend your performance budget on the destination page's critical path before you spend it on transition polish.

Where To Go From Here

This intro deliberately stops short of code patterns. The right next steps: read the at-rule reference, sketch a transition between two real pages on your site (typically an index and a detail page), and run it in Chromium with DevTools' Animations panel open. The panel lets you scrub the timeline and inspect every ::view-transition-* pseudo-element as a first-class animation track, which is by far the fastest way to internalise the model.

When you're ready to extend beyond a single hero-image tag, look at named transition groups for layout regions, the view-transition-class property for grouping animations by category, and the JavaScript event surface for cancelling transitions that would mislead the user. The platform is small but composes carefully, and every gotcha above maps to a specific section of the spec rather than a mysterious browser quirk. That's unusual — and welcome — for a CSS feature this visible.