Multi-page sites get a bad reputation for feeling clunky, and it’s almost always for one specific reason: the white flash. You click a link, the whole page goes blank for a fraction of a second, and then the new content pops in. It’s such a normal part of the web that most of us stopped noticing it years ago — right up until we use a site that doesn’t do it, and suddenly every other site feels dated by comparison.
That flash isn’t actually a law of nature. It’s just what happens by default when a browser swaps documents. And as of Chrome 126, Edge 126, Safari 18.2, and Opera 112, you can turn it off with one line of CSS.
@view-transition {
navigation: auto;
}
Enter fullscreen mode Exit fullscreen mode
Drop that into a stylesheet that’s loaded on every page of your site, and internal navigations stop hard-cutting between pages and start cross-fading. No JavaScript, no router, no framework — just the browser doing what it was always capable of doing, now that there’s a standard way to ask for it.
What’s actually happening under the hood
The View Transition API works by taking a screenshot of the page right before it navigates away, letting the new page load and render, and then blending the two together instead of just replacing one with the other instantly. The default is a simple cross-fade, but it’s a real transition — smooth, GPU-accelerated, and running independently of whatever your JavaScript is or isn’t doing.
The genuinely nice part is that this is progressive by design. Browsers that don’t support the API just… do a regular navigation. Nobody sees an error, nobody sees a broken animation. You either get the smooth version or the version the web has always had.
Same-page transitions, for SPA-style interactions
The one-liner above covers full page navigations, but the same underlying API works inside a single page too — toggling a panel open, swapping a tab, filtering a list:
toggle.addEventListener('click', () =>
document.startViewTransition(() => article.classList.toggle('open'))
)
Enter fullscreen mode Exit fullscreen mode
Whatever DOM change happens inside that callback gets wrapped in the same snapshot-and-blend treatment. The browser captures the “before,” runs your class toggle, captures the “after,” and animates between them automatically.
Making specific elements move, not just fade
The default cross-fade is fine for a lot of cases, but the effect that actually makes people stop and go “wait, how did they do that” is when one specific element — a thumbnail, a hero image, a card — visibly moves and resizes into its new position across the transition. That’s what view-transition-name is for:
img {
view-transition-name: hero;
}
Enter fullscreen mode Exit fullscreen mode
Give an element the same view-transition-name on both the old page and the new page, and the browser interpolates its position, size, and shape between the two states. This is the trick behind those “product thumbnail grows into the full product photo” transitions you’ve probably seen on shopping sites — no manual FLIP animation math, no library, just a matching name on two elements.
Writing your own transition instead of the default fade
If a plain cross-fade doesn’t match your brand, you can override it with pseudo-elements the browser generates automatically for you:
::view-transition-old(root) { animation: fade-out 0.3s ease-out; }
::view-transition-new(root) { animation: fade-in 0.3s ease-out; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }
Enter fullscreen mode Exit fullscreen mode
::view-transition-old is a snapshot of how things looked before the change, ::view-transition-new is how they look after — style each independently and you can build slides, wipes, scale effects, whatever fits your site instead of the default cross-fade.
Respecting people who don’t want the motion
Not everyone wants pages to animate, and some people genuinely can’t tolerate it comfortably. Wrap the one-liner in a media query and it only activates when the visitor hasn’t asked for reduced motion:
@media not (prefers-reduced-motion: reduce) {
@view-transition {
navigation: auto;
}
}
Enter fullscreen mode Exit fullscreen mode
For same-document transitions triggered from JavaScript, you can check the same preference before calling startViewTransition, or skip a transition that’s already started:
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.startViewTransition?.(() => {})?.skipTransition();
}
Enter fullscreen mode Exit fullscreen mode
You can also feature-detect support directly with CSS.supports('view-transition-name: x') if you want to branch your logic rather than relying purely on the progressive-enhancement fallback.
Where browser support actually stands right now
This is the part that goes stale fastest in any article about a newer web API, so here’s where things stand as I’m writing this:
Browser Same-document (SPA) transitions Cross-document (MPA) transitions Chrome / Edge ✅ ✅ (126+) Safari ✅ ✅ (18.2+) Opera ✅ ✅ (112+) Firefox ✅ (144+) 🟡 landing in pieces, full support expected via Interop 2026Firefox spent a long time as the clear holdout here, but that’s shifted — same-document transitions shipped in Firefox 144, and cross-document support (the piece the @view-transition at-rule actually needs for MPA navigations) is actively being rolled out rather than sitting untouched. Worth rechecking caniuse before you ship anything support-critical, since this is exactly the kind of status that shifts release to release.
Either way, because this whole feature is progressive enhancement, none of that uncertainty is actually a blocker. Visitors on a browser without support just get a normal navigation — the one they’d have gotten regardless.
A few things that catch people off guard
Not every navigation qualifies. Only same-origin navigations that don’t trigger a full reload get the transition — things like location.reload(), and navigations blocked by iframes or sandboxing, don’t qualify.
A stray beforeunload listener kills it silently. If your page — or a third-party script loaded on it — has a beforeunload handler attached, the transition gets cancelled with no error message. This one has cost people more debugging time than anything else on this list.
Chrome DevTools will actually tell you what went wrong. Open More Tools → Animations and it’ll show you exactly which transition ran, and flag what blocked one that didn’t.
Keeping it fast
A view transition is still fundamentally a screenshot-and-blend operation, so the usual performance rules apply, just with slightly different consequences:
- Keep the pages themselves lightweight — a heavy page makes the “after” snapshot slow to capture, which shows up as a stutter right at the point the transition should feel smoothest.
- If you’re triggering a same-document transition from JS, batch your DOM changes inside the
startViewTransitioncallback so the browser only has to re-lay-out once, not once per change. - Go easy on
filterandbox-shadowinside::view-transition-old/::view-transition-new— both are expensive on the GPU fill-rate, and you’re already asking the compositor to do extra work for the transition itself.
What’s coming next
Two things worth keeping an eye on if this clicks for you: the Navigation API pairing with View Transitions to make history manipulation properly flicker-free, and a proposed match-element value for view-transition-name that would let the browser pair up elements automatically instead of requiring you to hand-assign matching names on both pages.
For now, though, the one-liner alone is the highest ratio of “visual impact” to “lines of code” you’re likely to find in CSS this year. Add it to your stylesheet, ship it behind a prefers-reduced-motion check, and the next full page load on your site won’t flash white.
We’re ArtClick, a web development agency based in Kyoto. We build company websites, WordPress sites, and custom systems — with a focus on sites that are fast, well-designed, and easy to maintain long-term. Learn more at 🔗https://artclickdev.com/