
The Moment I Realized My SPAs Felt Like Static Sites (And How View Transitions Fixed It)
Stop letting jarring page jumps kill your user's flow; here’s how I finally bridged the gap between instant navigation and cinematic continuity.
Your user's brain shouldn't have to work overtime just to figure out where a piece of content went after they clicked a link. We’ve spent years making Single Page Applications (SPAs) lightning-fast, yet they often feel "cheaper" than native mobile apps because they lack the spatial continuity that tells a coherent story.
I used to think my React apps were peak UX because the data fetched in 100ms. But then I looked closer. A user clicks a card, the screen flashes, and—*poof*—they’re on a detail page. It’s technically instant, but it’s visually violent. It feels like a static site from 2005, just faster.
Then I found the View Transitions API, and I realized we’ve been doing way too much work for way too little payoff.
The "Blink" Problem
In a standard SPA, when you navigate, you’re essentially nuking one DOM tree and planting another. Even if your state management is perfect, the browser has no idea that the <img> on Page A is the same <img> on Page B. To the browser, it’s just more pixels to paint.
To fix this, we used to reach for heavy lifting libraries like Framer Motion or GSAP. They’re great, but they require you to orchestrate the "exit" of one component and the "entry" of another. It’s a lot of boilerplate for a simple crossfade.
The View Transitions API changes the game by taking a "snapshot" of the old state, waiting for you to update the DOM, taking a snapshot of the new state, and then animating between them.
The Bare Minimum to Get Moving
Here is the magic trick. Instead of just calling your router's navigate function or updating your state, you wrap it in document.startViewTransition.
const handleNavigate = (newUrl) => {
// Check for browser support
if (!document.startViewTransition) {
updateTheDOM(newUrl);
return;
}
// The browser takes a screenshot of the current page here
document.startViewTransition(() => {
// This is where the magic happens: the browser updates the DOM
// and prepares the "new" screenshot
updateTheDOM(newUrl);
});
};By default, the browser gives you a nice, subtle crossfade. It’s already 10x better than the jarring "jump" we’re used to.
Making Elements "Travel"
The crossfade is cool, but the real "Aha!" moment comes when you move specific elements across the screen. You know that effect where a thumbnail expands to become the header of the next page? That used to be a nightmare to code. Now, it’s one line of CSS.
You just need to give the element on Page A and the element on Page B the same view-transition-name.
/* On your list page */
.card-thumbnail {
view-transition-name: hero-image;
}
/* On your detail page */
.detail-header-image {
view-transition-name: hero-image;
}When the transition starts, the browser realizes these two elements share a name. Instead of fading them, it literally morphs the size and position of the first one into the second one. It feels like the app has physical depth.
A quick gotcha: These names must be unique on the page. If you have a list of twenty cards, don't give them all the name card-image. You’ll need to apply the style dynamically (inline styles are your friend here) so only the clicked element has that specific transition name.
Customizing the Vibe
The browser uses a set of pseudo-elements to handle these transitions. If you want to change how the animation feels—say, you want it slower or you want a spring effect—you target ::view-transition-old and ::view-transition-new.
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}I usually stick to the defaults for the "root" (the whole page) and save the custom animations for specific UI pieces like sidebars or modals.
What about Multi-Page Apps (MPAs)?
For a while, this was an SPA-only party. But Chrome (and soon others) recently added support for cross-document view transitions. If you're building a traditional site with Astro, Next.js (with some tweaks), or even just plain HTML, you can opt-in with a single meta tag or a CSS rule:
@view-transition {
navigation: auto;
}Just like that, your static-ish site starts behaving like a fluid, high-end mobile app.
Don't Forget the "Don'ts"
While I’m currently obsessed with this API, it’s easy to overdo it. Here are two things to keep in mind:
1. Respect User Preferences: Some people get motion sick from heavy transitions. Always wrap your custom animations in a media query:
css
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*) {
animation: none !important;
}
}
2. Performance Matters: Even though the browser handles the heavy lifting, animating huge layers can still cause dropped frames on low-end devices. Keep your transitions purposeful, not just "flashy."
The View Transitions API is the first time in years that I've felt like the web is finally catching up to the "polish" of native platforms without requiring a PhD in animation logic. It bridges that weird gap between "pages" and "experiences," and honestly, I'm never going back to the "blink" again.


