Web Architecture Next.js Static Architecture

View Transitions Are Finally Usable

Static Signal
A vast brass observatory with overlapping translucent panes of glass shifting between scenes, each pane revealing a different room frozen mid-morph, dark steampunk interior lit by neon copper filaments

For about a decade, the only way to get a smooth transition between pages on the web was to stop having pages.

You’d ship a single-page application, intercept every click on every link, run a router in JavaScript, fetch the next view as a fragment of state, and animate the DOM between two snapshots that the browser never knew were “different pages” at all. You gave up the back button (or rebuilt it). You gave up real URLs (or simulated them). You gave up multi-page architecture (or hid it). All so an image could grow gracefully into a hero on the next screen instead of snapping into place.

The cost was enormous and the benefit was visual polish. Most teams paid it anyway, because the alternative was a website that felt like 2008.

That tradeoff is over. The View Transitions API has shipped in every major browser, the cross-document version is now stable, and every meaningful framework has integration. You can write a static site — actual separate HTML documents, full page loads, no client-side router — and get the kind of morphing animations that used to require a SPA. The browser does the work.

This is one of those rare web platform moments where a thing the framework ecosystem invented gets absorbed into the browser, and the framework version starts to look like overhead.


What Changed

The View Transitions API is conceptually simple: you ask the browser to capture a snapshot of the current page, swap the DOM (or navigate to a new document), and then animate between the old snapshot and the new one. The animation is declarative — you target it with CSS — and it runs on the compositor thread, not on the main thread, so it stays smooth even when JavaScript is busy.

There are two flavors. Same-document transitions were the first to ship and have been usable in Chromium for a couple of years. They handle the SPA case: you’re already running JavaScript, you mutate the DOM, and the browser animates between before-and-after states. Cross-document transitions are the new and important part. They work between two completely separate page loads. You navigate from / to /about, the browser tears down the old document and builds the new one, and a transition runs across that boundary as if the two pages were one continuous experience.

Cross-document is the unlock. Same-document just made SPAs slightly nicer to write. Cross-document means you can throw away the SPA entirely and still have the animation.

The browser support story matured fast. Chromium had it first. Safari shipped same-document in 18 and cross-document in 26. Firefox, which spent years gesturing vaguely about “exploring the proposal,” landed it in their early 2026 release after the API stabilized. As of the spring, you can write a cross-document view transition and have it work for over 95 percent of users on the first paint, with graceful no-animation fallback for the rest. That is the real shift. The API has existed for a while; the universal support is what makes it usable.


The Basic Mental Model

Think of a view transition as a four-step pipeline the browser runs for you:

  1. Capture. The browser takes a snapshot of the current page (or specific elements you’ve marked).
  2. Mutate. The DOM changes — either through script (same-document) or through navigation (cross-document).
  3. Capture again. The browser snapshots the new state.
  4. Animate. A CSS animation cross-fades between the two snapshots, with optional per-element morphing.

The default animation is a cross-fade of the entire page. That alone is already nicer than the abrupt content swap browsers have done forever. But the interesting part is when you give specific elements a view-transition-name. Any element with a name that appears in both the “before” and “after” snapshots gets paired up, and the browser animates the geometry between the two — position, size, scale — for free.

So a hero image on a listing page that has view-transition-name: post-hero-42 will, when you click through to the post detail page where another image also has view-transition-name: post-hero-42, smoothly fly and resize from the listing position to the detail position. You wrote no JavaScript. You wrote two CSS lines. The browser handled the captures, the geometry math, the easing, and the compositing.

This is what shared-element transitions in native mobile apps have looked like for fifteen years. The web finally has it.


The Cross-Document Version, Concretely

For a static site, the entire opt-in is a single line of CSS in your global stylesheet:

@view-transition {
  navigation: auto;
}

That’s it. Every same-origin navigation in your site now runs through the view transition pipeline. The default animation kicks in immediately. You’ll see your pages cross-fading instead of snapping.

To upgrade specific elements to morph between pages, you assign matching transition names. Frontmatter-driven IDs work beautifully here, because the same logical element (a post hero, an author avatar, a nav item) renders on multiple pages with the same identifier:

.post-hero {
  view-transition-name: post-hero;
}

If only one post hero is visible at a time on each page, that single name is enough — there’s only one “before” element and one “after” element to pair. If you have multiple, generate unique names from your content IDs:

.post-card[data-slug="image-optimization"] .thumbnail {
  view-transition-name: hero-image-optimization;
}

There is no JavaScript. The browser sees the matching names across two separate document loads and animates the layout difference. On a static site, this means your build output is literally just HTML files, and the transitions are still smooth.

This is the part that took a decade.


Why This Matters Specifically for Static Sites

If you’re building a SPA, view transitions are a quality-of-life improvement. Your site already had no real page loads, you already controlled the transition timing, and the API just gives you a nicer primitive than custom-built FLIP animations.

If you’re building a static site, view transitions are a category change. They eliminate the single most common reason teams abandoned multi-page architecture: “we wanted SPA-feeling transitions.” That reason is no longer valid. You can have the architectural simplicity of separate HTML documents — the cacheability, the linkability, the resilience to JavaScript failure, the compatibility with edge caches, the SEO clarity — and still ship the polish that used to require building everything in React or Vue.

The architectural cost of going SPA was always huge: client-side routing, hydration overhead, state management for navigation, careful link interception, scroll restoration logic, accessibility regressions around announcing route changes, the boundary between the framework’s worldview and the platform’s. View transitions let you keep the platform’s worldview and add the polish on top.

This is the same pattern as loading="lazy" killing custom image lazy-loading libraries, or <dialog> killing custom modal libraries, or container queries killing JavaScript-driven responsive components. The browser absorbs the feature, and the framework code that used to provide it becomes legacy.


What the Frameworks Do Now

If you’re on Next.js with the App Router, view transitions for client-side navigation are wired in as of 16.x. You opt in per layout segment, give the relevant elements view-transition-name declarations, and the framework hands the navigation off to the browser’s transition pipeline. For static export builds, the cross-document API works without any framework involvement — every navigation is a real document load, and the @view-transition CSS rule is enough.

Astro shipped a <ViewTransitions /> component a while ago that papered over the API’s rough edges back when cross-document support was still landing. You can still use it, but for new sites the bare CSS approach is now equivalent. The component was a polyfill for a moment in time, and that moment is closing.

For React projects that aren’t on Next, there’s a useViewTransition hook pattern that wraps the same-document API for state-driven UI updates inside a SPA shell. It composes naturally with startTransition for the React side, and with document.startViewTransition for the browser side. The two have annoyingly similar names and entirely different jobs, which is going to confuse a generation of new developers, but the actual mechanics are clean.

The point isn’t to pick a framework. The point is that the API is the same underneath, and any framework that doesn’t get out of its way is now part of the problem.


What Goes Wrong If You’re Not Careful

Three failure modes show up in production code, and you’ll hit all three the first time.

Animating too much. The default transition cross-fades the entire page. That’s pleasant. But if you start naming dozens of elements, you’ll find that the browser dutifully animates every one of them, and the result looks like a slot machine. Pick the elements that genuinely benefit from being tracked across pages — the hero, the headline, maybe one thumbnail — and let everything else cross-fade as a group. Restraint here matters more than for any animation work you’ve done before, because the API makes it free, which makes overuse silent.

Reflows during the snapshot. If your “after” page is still loading data when the snapshot is captured — fonts not ready, images not decoded, layout shifting — the transition animates from the right starting state to a wrong intermediate state, and then snaps. The fix is to ensure the destination page is fully laid out before the transition runs. For static sites this is mostly a non-issue (the HTML is already complete), but for any same-document transition that depends on a fetch, you need to await the data before calling startViewTransition.

Accessibility around motion preferences. The API respects prefers-reduced-motion: reduce, but only for the default animations. The moment you write custom ::view-transition-* keyframes, you become responsible for honoring that preference yourself. Wrap your overrides in a media query or you’ll be inducing motion sickness in users who explicitly asked you not to:

@media not (prefers-reduced-motion: reduce) {
  ::view-transition-old(post-hero),
  ::view-transition-new(post-hero) {
    animation-duration: 400ms;
  }
}

This is the single most common omission in early adopter code right now. Don’t be that site.


When Not To Use This

The honest answer: most listing-to-detail navigations don’t need shared element transitions. The default cross-fade is a pure win — it’s free, it always looks better than a snap, and it requires no thought. But once you’re naming elements and choreographing morphs, you’re spending design and engineering time on something users will register subconsciously at most.

The places where it genuinely matters:

  • Image-heavy navigation where the same image appears on the source and destination page (gallery to detail, listing to post, product card to product page). The morph reinforces continuity in a way that pays for itself.
  • Persistent UI elements that should appear stable across navigation — a fixed sidebar, a sticky header, a player controls bar. Naming them prevents the visual blink that makes them feel reconstructed on every page load.
  • State-driven SPA UIs where the user is reordering, filtering, or expanding content, and the spatial relationship between before-and-after carries meaning.

The places where you should resist:

  • Marketing pages where every section is unique. Cross-fade is plenty.
  • Form-heavy applications where the next “page” is a different form. Animations distract from the task.
  • Long content pages where the snapshot capture is expensive and the morph is going to look weird because there’s so much novel content on the destination.

A good heuristic: if you can’t articulate what the morph is communicating, don’t write it.


What This Tells You About the Web Platform

There’s a recurring pattern in web development where a need emerges, the framework ecosystem invents three or four competing solutions, those solutions calcify into “best practices” that everyone copies, and then five years later the browser ships a primitive that does the same thing better and the framework code becomes a load-bearing legacy. We’ve watched it happen with lazy loading, with dialogs, with date pickers, with form validation, with intersection-based reveal animations. View transitions are the latest and possibly biggest example.

The lesson isn’t “don’t use frameworks.” Frameworks still solve real problems — component composition, type safety, build pipelines, server rendering. The lesson is that the parts of your stack which exist purely to compensate for missing browser features have a shelf life. The browser is going to ship the feature eventually. When it does, the framework code becomes the thing slowing you down.

For static sites this is a particularly happy outcome. The architectural simplicity of “separate HTML documents linked together” was always the correct model for content. The reason teams kept rebuilding it as a SPA was almost entirely visual — they wanted the polish that the platform didn’t yet provide. Now the platform provides it. The reason to go SPA for content sites has narrowed to almost nothing.


A Practical Adoption Path

If you want to try this on an existing static site, here’s the smallest useful diff:

  1. Add @view-transition { navigation: auto; } to your global stylesheet. Ship it. Every same-origin navigation now cross-fades.
  2. Identify your single most-shared element across pages — usually a hero image or a post thumbnail. Add a view-transition-name based on the post slug or ID. Ship it. Watch the morph happen.
  3. Wrap any custom transition timing in a prefers-reduced-motion query.

That’s a one-day change. It costs you no architecture, no dependencies, no JavaScript, and no migration. If you don’t like the result you remove three CSS rules. The downside is genuinely zero.

Most upgrades to web platform features are not this cheap. This one is.


The Real Takeaway

View transitions are not a small feature. They close a decade-long gap between what designers wanted and what the multi-page web could deliver, and they do it without forcing you to take on the structural complexity of a SPA. For static sites — the kind of sites that benefit most from the platform’s native model — this is the missing piece. You can stop apologizing for “just” rendering HTML. The browser will animate it for you.

If you’ve been holding onto a SPA architecture for content because the alternative felt visually flat, this is the moment to reconsider. The visual flat is gone. What’s left of the SPA case is the part that was always genuine — interactive applications with persistent state — and that’s a much smaller surface than what most teams currently ship.

The right response is to look at your codebase, find the parts that exist purely to provide animations the browser now provides for free, and start deleting them. There’s a lot to delete.