Skip to content
Animation

Page transitions with the View Transitions API

How the browser blends two page states into each other on its own, what two lines of CSS already buy you, how a single element survives a navigation and where the limits sit that no announcement mentions.

11 min readUpdated: 28 September 2026
View transitionsCSSAnimationNavigation
Ease-out
cubic-bezier(.22, 1, .36, 1)

Clicking a link is the harshest moment on a website. The old page vanishes, something white flashes up, the new page is suddenly fully there. Nobody ever said it has to be that way: it is simply how browsers have always navigated. The View Transitions API turns that sequence around. The browser takes a picture of the old state, a picture of the new one, lays them on top of each other and blends between them. This guide shows how far that now carries without a single line of JavaScript, how one element travels from the old state into the new one, and at which four points the whole thing still costs you work.

The short version
  • The browser grabs two pictures, one of the old state and one of the new, and puts them on a layer of their own above the page. Everything you do afterwards is a perfectly ordinary CSS animation on pseudo-elements.
  • For a navigation between two real HTML files, one block with navigation: auto is enough, and it goes into both pages. Same origin is a condition, JavaScript is not.
  • view-transition-name lifts an element out of the cross-fade: it gets a group of its own and travels from its old spot to its new one. The name has to occur exactly once per page, otherwise the entire transition is skipped.
  • The cross-document variant is still missing in Firefox, and a load that takes longer than four seconds aborts it. Neither is a bug in your code, both are the reason the transition has to stay an addition and never the scaffolding.
01

Why does a navigation feel harsher than a click?

Inside a page, motion has become a given. A panel slides open, a list resorts itself, an image grows on hover. The moment a link is clicked, all of that stops. The old page is discarded, the screen is blank for an instant, the new page stands there finished. Between the two states there is nothing explaining how you got from one to the other.

The reason is technical and obvious the moment you say it out loud: there is no moment in which both pages exist at once. The old one is gone before the new one starts. An animation needs exactly that, though, two states side by side. So every solution up to now has done the same thing: intercept the real navigation, fetch the content itself and artificially keep both states inside the same document. That is why page transitions were tied to a single-page application for so long.

The View Transitions API turns this around. Instead of keeping both pages alive at once, the browser holds on to two pictures: one of the old state, taken just before it disappears, one of the new one, taken just before it shows up. It puts those two pictures on a layer of their own above everything and animates them against each other. The page underneath is already the new one by then. What you see is an afterimage.

A page transition does not animate pages. It animates two pictures of pages.

02

What exactly does the browser do?

The sequence has four steps, and it is the same in both variants. The browser captures the old state as a picture. It builds the new state and captures a picture of that too. It puts both pictures on a layer of their own above the document. And it animates them, by default as a cross-fade over a quarter of a second.

That layer is not some hidden mechanism, it is a small tree of pseudo-elements you address with perfectly ordinary CSS. At the top sits ::view-transition, below it one ::view-transition-group per named part, inside that a ::view-transition-image-pair and finally ::view-transition-old and ::view-transition-new. With no further instructions there is exactly one group, and it is called root: the whole visible area as a single picture.

Because these are pseudo-elements, everything you know about CSS animations applies. animation-name, animation-duration, animation-timing-function, keyframes of your own. There is no new language for motion and no separate timeline. There are only two extra pictures you can get at.

initial
opacity 0, y 12
animate
opacity 1, y 0
duration
0.5 s
ease
cubic-bezier(.22, 1, .36, 1)
stagger
80 ms

Twelve pixels of travel and half a second. A hero needs no more.

The old picture leaves, the new one arrives. For the length of the transition both sit on the same layer above the page.

::view-transition-old(root) {  animation: 220ms cubic-bezier(0.4, 0, 1, 1) both fade-out;} ::view-transition-new(root) {  animation: 320ms cubic-bezier(0.16, 1, 0.3, 1) both rise-in;} @keyframes rise-in {  from { opacity: 0; translate: 0 1.5rem; }}
Giving the default a movement of its own. That is all it takes.
Look first, then writeThe Chromium developer tools can pause a running transition and show the pseudo-element tree along with its animations. That is the fastest route to answering why some part does not move the way you expected: usually you see straight away that it has no group of its own and is therefore riding along with the cross-fade of everything.
03

How much CSS does a real navigation need?

Less than you would expect. For the move between two real HTML files, both pages carry one block containing a single declaration, and that is the setup done. No script, no router, no intercepted click. The browser recognises the navigation itself and puts the transition in between.

Two conditions come with it. Both pages have to share an origin, meaning the same scheme, hostname and port. And both have to contain the rule: the old page thereby allows its picture to be captured, the new one allows itself to serve as the counterpart. If either side is missing, nothing happens, and it happens silently.

Which navigations count is decided by the value. With auto it is the routes originating in the page itself: a click on a link, a submitted form, the back and forward buttons. Not included is anything coming from the browser interface, meaning the address bar, a bookmark and a reload. That is a sensible split: a transition is meant to show movement within a session, not the act of entering the site from outside.

  @view-transition {    navigation: auto;  } /* optional: die Voreinstellung überschreiben */::view-transition-group(root) {  animation-duration: 280ms;}
In both pages. That is the complete setup.
The old syntax is retiredThe earliest write-ups on this topic put a meta tag in the head of the page, with name="view-transition" and content="same-origin". That version has been replaced and no longer does anything. It does not say goodbye either: there is no console warning, the transition simply fails to appear. If you followed an older tutorial and wonder why nothing happens, look for that tag first and rewrite the rule as a CSS block.

Two pages, the same rule. If either forgets it, there is no transition.

04

How does a single element travel along?

Up to here the whole screen is a single picture. It gets interesting the moment one part of it takes on a role of its own. That is precisely what view-transition-name does: the element is lifted out of the big cross-fade and gets a group of its own on the layer above.

If the browser finds the same name in both states, it treats the two occurrences as the same thing. It compares position and size and animates the group from the old spot to the new one, while the two pictures inside it blend into each other. That is the effect you know from image galleries: a small thumbnail in the overview becomes the large image on the detail page, and the way in between is visible. It costs two declarations, one per page.

One rule there is strict: the name has to be unique within a document. If it turns up twice, say because all twelve cards in an overview carry the same class, the browser cannot form the pairs and skips the entire transition. Not just that one group, the whole thing. In a list the name therefore belongs on the one element in question, not on the class. Where the names come from data, you set them through a custom property or read them out of an attribute with the typed form of attr. Shared looks across many groups are described by view-transition-class instead, which needs no name handed out twice.

/* Übersicht: nur die angeklickte Karte bekommt den Namen */  .karte[data-aktiv] .bild {    view-transition-name: beitragsbild;  } /* Detailseite: dasselbe Bild, dieselbe Bezeichnung */  .beitrag > img {    view-transition-name: beitragsbild;  } /* verzerrte Bilder geraderücken */::view-transition-old(beitragsbild),::view-transition-new(beitragsbild) {  object-fit: cover;  height: 100%;}
The same name in both pages. The browser computes the way in between.
Leads to skipped transitions
  • Putting the name on a class that occurs twelve times in the overview. The browser finds twelve elements with the same name and drops the entire transition.
  • Naming an element that does not exist on the destination page. Then only half the pair is there, and instead of travelling you see it disappear.
  • Controlling the image size through a class on the element. The pseudo-elements do not inherit that, and with differing aspect ratios the picture gets squashed during the transition.
  • Naming every visible part of the page so as much as possible travels along. Dozens of groups come into being and all run at once, and the result reads as restless rather than connected.
Leads to calm transitions
  • Setting the name on exactly the one element that was clicked, shortly before the navigation, for instance through an attribute on an ancestor.
  • Naming three or four parts that exist on both pages: the image, the headline, the header area. Everything else cross-fades along.
  • Setting object-fit on the pseudo-elements themselves, so the aspect ratio holds over the whole way.
  • Deriving names from the data when several elements are candidates, and handing them to CSS through a custom property or attr.
A duplicated name costs the entire transitionIt is tempting to write the name onto the card class, because that looks shortest. The result, though, is not a transition with a flaw in it, it is no transition at all, and the cause does not show up in the console. So if the default suddenly turns abrupt again, look first for a name that appears more than once in the document.

Three curves cover everyday work.

A page with three curves feels ordered, one with twelve feels random. These three are almost always enough.

Soft0.22, 1, 0.36, 1Anything entering
Symmetric0.4, 0, 0.2, 1Open and close, back and forth
Calm0.32, 0.72, 0, 1Long distances across the page
05

How does the transition know which way it is going?

A transition that always looks the same quickly loses its point. A step deeper and a step back should differ, otherwise by the third click you no longer know where you are. That is what types are for: names you attach to the transition and query again in CSS.

Fixed types sit right in the rule, next to the navigation. The movable ones are more interesting. During a navigation two events fire, pageswap on the old page just before its last frame and pagereveal on the new one before it first appears. Both hand you the transition object, and there you can add a type, derived for instance from a comparison of the two addresses: where you came from sits in navigation.activation, where you are going in location. Is this a level deeper or a level back? You then query it with the :active-view-transition-type pseudo-class.

Alongside direction comes duration, and that is the part most transitions fail at. A navigation must not feel like a waiting room. The browser default sits at a quarter of a second, and that is a good bearing: between 200 and 350 milliseconds for a cross-fade, towards the upper end for an element covering a visible distance. Anything beyond half a second gets in the way of a person who only wanted to reach the next page.

0.15 sToo short. You see a jump.
0.5 sThe everyday value for fades.Recommended
1.6 sToo long. You wait for the interface.

Short reads as assured, long reads as loading time. For a navigation the usable range sits between 200 and 350 milliseconds.

window.addEventListener("pagereveal", (e) => {  if (!e.viewTransition) return;  const von = navigation.activation.from?.url;  const tiefe = (u) => new URL(u).pathname.split("/").length;  const tiefer = !von || tiefe(location.href) > tiefe(von);  e.viewTransition.types.add(tiefer ? "vorwaerts" : "zurueck");});   html:active-view-transition-type(zurueck) {    &::view-transition-old(root) { animation-name: nach-rechts; }  }
The direction comes from the address, the motion from the CSS.
:active-view-transition tidies the page upWhile a transition runs, exactly the useful things get in the way: an open menu, a sticky header, a hint that happens to be showing. The :active-view-transition pseudo-class matches the root element while a transition is running, and only then. That lets you hide such content for the length of the move without setting a class and removing it afterwards. Forgetting to clean up is not possible on this route.
06

What does the API not take over?

The gain is large, and it has clear edges. The API describes a moment between two states, not the behaviour of an application. Four points stay work, and three of them are not a matter of code but of expectation.

  1. Cross-document is missing in Firefox. The variant inside one document is available everywhere, the one between two documents ships in Chromium and Safari. That is no reason to wait: where the rule is not understood, the browser navigates the way it always did. It is a reason never to let the transition carry information, though.
  2. Four seconds is the ceiling. If the new page needs longer to its first frame, the browser gives up and navigates the hard way. That turns the transition into an honest readout of your own load time: if you rarely see it on a phone, you do not have an animation problem, you have a speed problem.
  3. The pictures are pictures. What the browser captures is a snapshot, not a living copy. If the aspect ratios differ between the old and the new state, it squashes, and it keeps squashing until you set object-fit on the pseudo-elements themselves. On the element it does nothing.
  4. A router has to speak up. As soon as an application handles navigation itself, there is no page change left for the browser to recognise. There, document.startViewTransition wraps the moment the new state is set. Many routers have long shipped a switch for it, and underneath sits exactly that one call.

What the API does get right on its own is easily overlooked: it keeps the motion off the main thread. The cross-fade runs on the compositor while the new page is already finished. A hand-built solution always struggled at exactly this point, because it had to animate while content was being loaded and mounted in parallel.

The transition is an addition. The moment a page stops making sense without it, something else went wrong.

07

Where is a transition worth it, and where does it get in the way?

Not every navigation gets better with motion. The benefit depends on whether there is a relationship between the two states worth showing at all. Four cases stand clearly apart.

  1. Worth it right away. From an overview into a detail view. The clicked image grows into its new spot, and without a word that explains what you just clicked on. The same holds for product lists, galleries and article indexes.
  2. Worth it in small doses. Between pages on the same level, say in the main navigation. There the default with a curve of your own and a slight shift is plenty. Travelling elements are not needed, because there is nothing to follow between the pages.
  3. Stays without. Form flows, checkout steps, anything you walk through several times in a row. A motion you see for the tenth time stops being an explanation and becomes a delay.
  4. Needs something else first. Pages that take more than a second to their first frame on a phone. There the time limit kicks in regularly and the transition shows up arbitrarily. First the load time, then the motion.

A usable test is whether the same thing can be shown in both states. If there is an image, a headline, a surface present before and after, then a transition contributes something, because it makes a connection visible. If there is not, what remains is a cross-fade, and a cross-fade is rarely wrong but rarely important either.

Show a connection that genuinely exists. Anything else is decoration with a waiting time.

Share
Written by
Amelie RoesmannCreative directionLeonie RoesmannDesign engineering

We build websites and web apps at Systra Studios in Münster and put the building blocks from that work here. What you read comes from real projects, not from a brochure.

Systra Studios: web design from Münster

You know the curves.
You do not have to guess the values.

A collection with previews: look at the curve, play it, copy the value. The same value for CSS and for framer-motion.

  • Every curve plays before you take it.
  • One click copies the cubic-bezier value.
  • The same values sit in every component.
Browse easings
Soft0.22, 1, 0.36, 1
Symmetric0.4, 0, 0.2, 1
Calm0.32, 0.72, 0, 1
No easing0, 0, 1, 1
FAQ

Frequently asked questions

For the transition itself, no. For everything around it, often yes. The core of the matter, holding on to two states and moving one into the other while the new page is already built, cannot really be rebuilt in JavaScript: at that moment you never have both states in the document at once. That is exactly the part the browser now handles. What a library still does better is motion within a page: an element reacting to a gesture, a list resorting itself while filtering, an animation that changes direction mid-flight. The two do not rule each other out. In practice the API takes over the move between two pages, and the library stays responsible for what happens on the page afterwards.

Didn't find your question? We're happy to help.

Get in touch