• craft
  • animation
  • performance

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.

Dec 22, 2025·5 min read

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:

app/globals.css
.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:

app/globals.css
::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.