View transitions in practice (and where they still hurt)
The View Transitions API feels magical in a five-line demo. Here is what actually happens when you try to ship it on a real content site.
Native view transitions are one of the few recent browser APIs that reduce the amount of JavaScript I ship. After months of trying to use them on a real content site — not a demo — I have a shorter list of opinions than I expected.
#The two patterns I actually use
#Pattern 1: one element, one name
Give a single element the view-transition-name, leave everything else alone.
This is 80% of what I use the API for. A post card title morphing into the
post page title is the classic case:
.post-card .title {
view-transition-name: post-title;
}That is the entire thing. The browser handles the pairing; you write zero JS. When the user navigates, the title from the outgoing page finds the matching name on the incoming page and animates between them.
#Pattern 2: a tamer timing curve
The default browser animation for view transitions is a fast crossfade with an aggressive curve. It’s loud. The second thing I do on every project is override it:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 280ms;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}That’s the same curve I use for card hovers, and the same duration. Motion that reads as part of a design system, not a browser feature.
#Where it still hurts
#prefers-reduced-motion
The default crossfade counts as motion. If a user has reduced-motion on, you need to explicitly disable the transition — the API doesn’t do it for you:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
}#Same-origin only
Cross-document transitions need the @view-transition { navigation: auto; }
opt-in, and both pages have to declare it. That means you can’t easily
get them to work across an app boundary — say, marketing site to app shell —
unless both sides buy in. In practice, this is fine for content sites and a
nightmare for product apps.
#Shared names must be unique per render
This one bit me: if two elements share the same view-transition-name on the
same page, the browser throws a DOMException and cancels the entire
transition. That’s why post cards on a blog index need slugs in the
name:
.post-card[data-slug="foo"] .title { view-transition-name: post-foo; }Not view-transition-name: post-title. Every one of those names is a promise
that the element is unique in the viewport.
#The real win
The thing I keep coming back to: view transitions finally give you the native-app feeling — that a thing you clicked became the next page — without shipping a framework router or a JS animation library. That’s a big deal. The web has spent a decade trying to simulate this with bundles bigger than the page itself.
Is the API still rough? Yes. Would I ship it today on a content site? Yes, behind the reduced-motion guard. Would I bet a product experience on it? Not yet.
Use native where you can, JS where you must. View transitions moved a chunk of "must" into "can" — and that’s the quiet kind of progress the web rarely advertises.