View Transitions API: Honest First Impressions After One Real Project
I finally used the View Transitions API on a real project instead of in a toy demo. That project shipped. Real code, real users, real constraints.
I went in expecting magic. I came out with something more boring, but honestly more useful.
Short version: the View Transitions API is not the single-page-app killer the demos teased. I think it shines in exactly one scenario right now. When you hit that scenario, it feels fantastic. Outside of it, I would not bother yet.
The project: not a playground, an actual site
This was a small marketing site with a custom CMS-backed frontend. Pretty traditional stack:
- Static HTML generated at build time
- A sprinkle of vanilla JS for navigation, forms, and a couple of widgets
- No React, no Vue, no fancy routing layer
Client request: "Can we make the page transitions feel smoother? It feels janky when you click around." Classic. They did not ask for view transitions specifically. They just wanted that high-end app feel without rebuilding the whole thing as an SPA.
So I used this project as an excuse to try the View Transitions API properly. Not copying snippets from MDN. Actually integrating it into a codebase that has real content, varying layouts, and legacy CSS.
What the demos promise vs reality
The conference / Twitter demos all show the same thing:
- A card expands into a detail page
- Elements smoothly morph across routes
- You write a tiny bit of JS and a declarative CSS animation
The pitch is basically: "Give the browser your before and after DOM, and it does the rest. Instant SPA-level transitions with MPA simplicity."
Reality is slightly different when your site is not a hand-crafted demo that you built backwards from the animation you wanted.
I hit three main frictions almost immediately:
- Real layouts rarely align nicely between pages.
- SSR / static HTML means you do not always have easy control over timing.
- Existing CSS can fight the default snapshots in surprising ways.
The API still works. It is not broken. But that "just works" feeling is fragile. You get it in very specific conditions. Everywhere else, you are patching edge cases and falling back to instant hard cuts.
The one place it did feel great
I found one case where the View Transitions API really made sense and pulled its weight: navigation between structurally similar pages that share a layout, content density, and general visual rhythm.
On this site, that was the primary marketing pages:
- Home
- Features
- Pricing
- About
All of these shared:
- A sticky header
- A hero section with a title and short copy
- Sections stacked vertically with similar spacing
This meant I could treat the whole thing as a single "view" and just animate the page swap. Not element-to-element morphs. Just “old page slides/fades out, new page slides/fades in” with the header staying anchored.
That is the key detail. I stopped trying to recreate those hyper-granular card-to-detail transitions. I used it for full-page transitions on similar pages. Once I accepted that constraint, it felt pretty good.
How I wired it in
I went with the simplest usable setup first. Plain JS. No router library. Just progressive enhancement on normal links.
The core idea:
- Intercept clicks on internal links.
- Fetch the next page with
fetch(). - Swap the
<main>content inside adocument.startViewTransitioncall.
The rough version of the JS looked like this:
if (!document.startViewTransition) {
// No support, keep default navigation
} else {
document.addEventListener('click', async (event) => {
const link = event.target.closest('a');
if (!link) return;
const url = new URL(link.href);
const sameOrigin = url.origin === window.location.origin;
const sameTab = !link.target || link.target === '_self';
if (!sameOrigin || !sameTab) return;
event.preventDefault();
const response = await fetch(url.href, { headers: { 'X-Requested-With': 'view-transition' } });
if (!response.ok) {
window.location.href = url.href;
return;
}
const html = await response.text();
const parser = new DOMParser();
const nextDoc = parser.parseFromString(html, 'text/html');
const nextMain = nextDoc.querySelector('main');
const currentMain = document.querySelector('main');
if (!nextMain || !currentMain) {
window.location.href = url.href;
return;
}
document.startViewTransition(() => {
currentMain.replaceWith(nextMain);
window.history.pushState({}, '', url.href);
document.title = nextDoc.title;
});
});
}
That is almost the bare minimum. It is also where the first really useful part shows up.
startViewTransition takes a callback. Inside that callback you do your DOM swap. The browser grabs a snapshot before and after, then runs the transition. That control point is gold for this use case.
Styling the transition without losing my mind
Once the plumbing worked, I added some CSS. Again, I went minimal on purpose. Just enough to make it feel intentional, not like a random fade bug.
I settled on this:
- Fade and slight vertical slide between pages
- Header stays put
- Transitions are quick, not cinematic
::view-transition-group(root) {
animation-duration: 220ms;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}
::view-transition-old(root) {
animation-name: fade-out-slide-up;
}
::view-transition-new(root) {
animation-name: fade-in-slide-up;
}
@keyframes fade-out-slide-up {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-4px);
}
}
@keyframes fade-in-slide-up {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
I kept the distance small on purpose. When you exaggerate the motion you start to see layout differences and content jumps more clearly. Short transitions hide those, which is exactly what I wanted here.
For the header, I marked it as its own transition group and effectively disabled animation:
header {
view-transition-name: site-header;
}
::view-transition-group(site-header) {
animation: none;
}
This keeps the header visually pinned. Old header and new header overlap, with no animated delta. You do not notice the swap because the content and height are identical. Of course that relies on your header being very consistent between pages, which is exactly the type of constraint that makes the API behave nicely.
Where it fell apart
Then I tried to get fancy. I thought: "What if the feature cards on the features page smoothly morph into the hero on the pricing page?" That is the kind of thing the demos show off.
I tried wiring up view-transition-name on a card and on a matching element on another page. Still theoretically simple.
In practice I ran into:
- Cards that did not exist on every page
- Varying counts and breakpoints
- Responsive grid changes that kicked in during the snapshot
So the transitions looked good at exactly one viewport width, between exactly two pages, and only when the content length was similar. That is the “magic demo” territory. It is not how a normal content site behaves over time.
Maintaining this long term would be painful. Editors change copy. Marketing adds a new card. Someone tweaks a breakpoint. Suddenly the carefully named element transition animates something that no longer maps cleanly, and your “premium feel” turns into a weird glitch.
That is the moment I pulled back and went, okay, I am not building a design museum piece here. I am building a site that should be safe for people to change without everything subtly breaking.
The real constraints that matter
Here is how I would describe the reachable sweet spot after this project:
- Full-page transitions on structurally similar pages feel great.
- Element-to-element morphs across complex layouts are fragile.
- Anything highly dynamic or editor-driven is risky to tie to detailed transitions.
So I made a hard rule for myself in this codebase:
- Only animate full-page swaps for core marketing pages.
- No named transitions for content blocks that can change unpredictably.
- Short durations. Always under 250ms.
With that constraint set, the View Transitions API stopped being a science project and started being a small UX upgrade that I trust.
How it compares to SPA-style transitions
I have built motion-heavy SPA shells in React and Vue before. Framer Motion, React Transition Group, custom page transition managers etc. The pattern is familiar.
Compared to that, here is how I see it:
- Less control. You are letting the browser snapshot and layer things. That is good until you want to micromanage stacking, masking, and scroll behavior across many components.
- Less wiring. No explicit route transition framework, no “exit / enter” dance across components. The DOM swap inside
startViewTransitiondoes all that in one place. - Better for MPA with light JS. If your site is mostly server rendered or static and you only want a bit of polish, this fits naturally.
If I was already building a heavy SPA, I would probably still reach for a dedicated animation library rather than try to force View Transitions across a virtual DOM. For this marketing site, though, adding a whole front-end framework just for page transitions would have been ridiculous.
Performance and jank
Performance-wise, it held up better than I expected. The transitions stayed at 60fps on a mid-range laptop and recent mobile devices.
Where I did see issues:
- Occasional delays when the next page HTML took too long to arrive
- Heavier pages with large images causing a visible lag before the transition kicked in
I experimented with a small loading guard: if the fetch() took more than 300ms, I would skip the transition entirely and do a normal navigation. It is not perfect but it avoids the "click, wait, then sudden animated swap" feeling.
That alone hints at the current limitation: smooth view transitions rely on your navigation being fast in the first place. The API does not magically fix a slow backend or 4MB hero image.
Browser support reality check
Support is pretty decent in Chromium-based browsers. Others are catching up, but I treated it as a progressive enhancement from day one.
If document.startViewTransition does not exist, the site behaves like a normal multi-page site with regular link navigation. No FOMO. No polyfills.
This was refreshing. I did not have to maintain separate animation paths. The CSS hooks only do anything when the browser actually supports the snapshots. That is one thing I genuinely like about this API.
Would I use it again?
Yes, but only in very specific conditions.
I would reach for the View Transitions API again when:
- The site is an MPA with mostly static layouts.
- I control most of the markup and spacing.
- The main goal is subtle page-to-page polish, not dramatic storytelling animations.
I would not bother when:
- The content is highly dynamic or editor-driven.
- The layouts differ a lot between routes.
- I need fine-grained choreography between many small components.
If you are walking into it expecting those crazy expanding card demos, you will probably be disappointed once you plug it into a real site. If you treat it as a small tool for smoothing out similar pages, it is actually nice.
How I would start if I had to do it again
If I was adding View Transitions to another project tomorrow, I would do it in this exact order:
- Identify 2–3 routes with almost identical layout structure.
- Wire up the simplest possible
startViewTransitionwrapper around a<main>swap. - Add a short fade/slide on the
rootgroup only. - Exclude header/footer from heavy animation, keep them pinned or barely moving.
- Stop there and ship. Only then push further if there is a clear need.
This API rewards restraint. The more clever you try to be, the more edge cases you inherit. It is a bit like CSS grid in that way. Looks like it can do everything. Realistically, you use 20 percent of it for 80 percent of the benefit, and that is fine.
Honest summary
After one real project, my take is simple.
The View Transitions API is not the magical "SPA feeling without any SPA work" tool a lot of demos suggested. It is a solid progressive enhancement for specific layouts that you control tightly.
Use it for:
- Smoothing navigation between similar marketing pages.
- Short, subtle page-level transitions that do not fight the content.
Be very cautious about:
- Mapping lots of individual elements across routes.
- Tying animations to dynamic or editor-owned content.
- Expecting it to bail you out of slow navigation or bloated pages.
I am glad I used it. I am keeping it in the toolbox. I am just not rebuilding my mental model of web app architecture around it yet.
If you have a clean MPA and want a little extra polish, this is probably the nicest low-effort win you can ship right now. As long as you treat it like a scalpel, not a new religion.
Subscribe to my newsletter to get the latest updates and news
Member discussion