In Half 1, we coated the gotchas that chew you first: the deprecated meta tag that silently does nothing, the 4-second timeout that kills transitions with out telling you, the picture distortion that turns each side ratio turn into foolish putty, and the pagereveal/pageswap occasions that provide you with hooks into the transition lifecycle.
All of that will get you from “nothing works” to “one ingredient transitioning properly between two pages.” Which feels nice. For about 5 minutes. Then you definately attempt to construct a product itemizing web page with 48 playing cards that every must morph right into a element view, and also you notice the tutorials ignored the laborious half.
That is the place it will get actual. Let’s scale this factor.
Cross-Doc View Transitions Collection
- The Gotchas No person Mentions
- Scaling View Transitions Throughout A whole lot of Components (You’re right here!)
The Dream: One Line, Infinite Names
In an ideal world, you’d resolve the scaling downside with pure CSS. No JavaScript. No server-side loops. Simply this:
.card {
/* Generates card-1, card-2, card-3, and many others. robotically */
view-transition-name: ident("card-" sibling-index());
}
That’s ident() — a CSS operate proposed by Bramus (who works on Chrome) to the CSS Working Group. It takes strings, integers, or different identifiers, concatenates them, and spits out a sound CSS title. Pair it with sibling-index(), which returns a component’s place amongst its siblings (1, 2, 3…), and also you get auto-generated distinctive names for each ingredient in a listing. One rule. Works for 10 playing cards or 10,000. The CSS doesn’t care.
And it’s not simply view transitions. The identical sample works for scroll-timeline-name, container-name, view-timeline-name — anyplace you want distinctive identifiers at scale. You might even pull names from HTML attributes with attr() as an alternative of sibling-index(), setting up identifiers like ident("--item-" attr(id) "-tl"). The pliability is actual.
Right here’s the factor: half of this equation already exists. sibling-index() shipped in Chrome 138 — you need to use it as we speak for issues like staggered animations and calculated kinds. The lacking piece is ident(). There’s a Chrome Intent to Prototype from Could 2025, which implies it’s on the radar. However “on the radar” and “in your browser” are very various things. No browser ships ident() but, and there’s no timeline for when it’ll land.
So we will’t use it but. Nevertheless it’s value understanding about as a result of as soon as ident() ships, an enormous chunk of the complexity you’re about to see simply… evaporates. Till then, right here’s the way you resolve the identical downside effectively as we speak — with the instruments that truly exist in browsers proper now.
100 Merchandise, 100 Names, 1 Nightmare
Right here’s what occurs once you comply with a tutorial that reveals one hero picture transitioning between two pages and attempt to apply that sample to a grid:
/* THE NIGHTMARE - one rule per merchandise, ceaselessly */
::view-transition-group(card-1),
::view-transition-group(card-2),
::view-transition-group(card-3),
::view-transition-group(card-4),
::view-transition-group(card-5),
::view-transition-group(card-6),
::view-transition-group(card-7),
::view-transition-group(card-8)
/* ... think about 92 extra of those */ {
animation-duration: 0.35s;
animation-timing-function: ease-out;
}
::view-transition-old(card-1),
::view-transition-old(card-2),
::view-transition-old(card-3)
/* kill me */ {
object-fit: cowl;
}
That’s what you find yourself with in case you comply with the tutorials that solely present one or two named parts. They assign view-transition-name: hero to at least one picture and name it a day. Cool. Now attempt constructing a product grid.
Each view-transition-name on a web page should be distinctive. That’s a tough rule — if two parts share a reputation, the browser doesn’t know which one maps to which on the following web page, so it throws the entire transition out. On a list web page with 48 merchandise, you want 48 distinctive names. On a photograph gallery with 200 thumbnails, you want 200. The names aren’t the issue — you’ll be able to generate these. The issue is that each pseudo-element selector in your CSS targets a particular title, so your animation kinds explode into an unmanageable wall of selectors.
That is the place you have to perceive the distinction between two properties that sound like they do the identical factor however completely don’t.
Identify vs. Class: The Distinction That Adjustments All the things
And yeah, the naming right here is complicated. I’ll be trustworthy: the primary time I noticed view-transition-name and view-transition-class subsequent to one another, I assumed they have been interchangeable. They’re not, and the distinction issues.
Identify = id. It solutions: “Which ingredient on Web page A is the identical ingredient on Web page B?” Whenever you give a thumbnail view-transition-name: card-7 on the grid web page and provides the hero picture view-transition-name: card-7 on the element web page, you’re telling the browser these are the identical factor and to animate between them. Names should be distinctive per web page. Two parts can’t each be card-7 or the entire thing breaks.
Class = styling hook. It solutions: “How ought to the animation look?” When fifty parts all have view-transition-class: card, you’ll be able to write one CSS rule that controls the length, easing, and object-fit for all of them. It’s the identical psychological mannequin as CSS courses on common parts — .btn doesn’t determine a selected button, it says “type me like a button.”
Consider it like a database. The title is the first key — distinctive, identifies one particular row. The class is a class column — teams rows collectively so you’ll be able to run a question throughout all of them without delay.
Right here’s what that appears like in apply:
There it’s. Six playing cards, six distinctive names, however precisely three CSS guidelines dealing with all of the animation conduct. Might be sixty playing cards. Might be 600. The CSS doesn’t change.
The important thing line is that selector: ::view-transition-group(*.card). The asterisk is a wildcard for the title, and .card matches the view-transition-class. It reads as “any view transition group whose ingredient has view-transition-class: card, no matter what its particular title is.”
For cross-document multi-page software (MPA) transitions, the sample is similar however you generate the names on the server:
/* ONE stylesheet, shared by all pages, handles each product */
@view-transition {
navigation: auto;
}
::view-transition-group(*.card) {
animation-duration: 0.35s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
::view-transition-old(*.card),
::view-transition-new(*.card) {
object-fit: cowl;
}
That’s the complete animation stylesheet for a web site with hundreds of merchandise. Three guidelines. Regardless of what number of gadgets you might have within the database, you by no means add one other line of transition CSS.
Earlier than view-transition-class existed, folks have been doing horrifying issues — looping by way of gadgets in JavaScript to generate blocks with a whole lot of selectors, or utilizing CSS preprocessors to spit out each potential title permutation at construct time. It labored, technically, the identical manner duct-taping a automobile bumper works technically.
view-transition-class is the spec authors acknowledging that the unique API simply didn’t scale, and fixing it the fitting manner.
One gotcha: view-transition-class was added to the spec later to repair these actual scaling points. The property landed in Chrome 125 and is now in Chrome, Edge, and Safari 18.2+. Older Chromium variations and Firefox received’t acknowledge it but. The transitions will nonetheless work, they’ll simply use the default fade animation as an alternative of your customized timing. Not the worst fallback.
You too can assign a number of courses to a single ingredient, similar to common CSS courses. One thing like view-transition-class: card featured is legitimate, and you may goal it with both ::view-transition-group(*.card) or ::view-transition-group(*.featured). Helpful once you need most merchandise to transition the identical manner however want a couple of to face out with a unique animation type.
Don’t Identify All the things Upfront
All the things up to now has had view-transition-name sitting proper there within the HTML or CSS from the second the web page masses. That works. Nevertheless it has a price that’s not apparent till you hit real-world scale.
Have a look at the CSS for each pages. Zero view-transition-name declarations. None. Each card within the grid is nameless till the precise second the consumer clicks one.
Right here’s why that issues. Whenever you put view-transition-name on a component in your stylesheet — simply sitting there in CSS, assigned from web page load — you’re telling the browser, “This ingredient participates in each transition that occurs on this web page.” Each single navigation. The browser has to snapshot it, calculate its place, and arrange the pseudo-element tree for it. For one hero picture, who cares? For a grid of 48 product playing cards, that’s 48 parts being individually captured, diffed, and animated when the consumer solely clicked one of them. The opposite 47 snapshots are pure waste.
On a quick machine you won’t discover. On a mid-range Android telephone loading a grid of product pictures over LTE? You’ll really feel it. The transition stutters or the browser simply skips it completely as a result of it may possibly’t set the whole lot up quick sufficient.
The repair is to deal with view-transition-name like a just-in-time factor. Assign it for the time being of interplay, not at web page load.
The lifecycle goes like this:
- Person clicks a card on the itemizing web page.
- Browser begins navigating —
pageswapfires on the outdated web page. - Your
pageswaphandler seems atoccasion.activation.entry.urlto determine the place the consumer goes, finds the clicked card, slapsview-transition-name: product-42on it. - Browser snapshots that one named ingredient (plus the default
roottransition). - Navigation occurs, new web page masses.
pagerevealfires on the incoming web page.- Your
pagerevealhandler reads the URL, finds the hero ingredient, assigns the matchingview-transition-name: product-42. - Browser sees matching names on outdated and new snapshots — morphs between them.
- Transition finishes, your
.completedpromise resolves, you clear the names.
That’s it. One ingredient named, one ingredient transitioned, zero waste.
The occasion.activation object is your finest good friend right here. On the outgoing web page, occasion.activation.entry.url tells you the place the navigation is headed. On the incoming web page, you simply learn window.location. Between the 2, you might have the whole lot you have to determine which ingredient to call with none world state, no sessionStorage tips, no question parameter gymnastics past what your app already makes use of.
And about that cleanup step, eradicating the title after .completed resolves? It’s not simply tidiness. If the consumer navigates again to the itemizing web page and clicks a completely different card, you don’t need the outdated card nonetheless carrying a reputation from the earlier transition. Stale names trigger duplicate-name conflicts (on the spot transition dying) or wrong-element matching (the brand new web page morphs from the incorrect card). Clear up after your self.
This sample is principally what Astro’s transition:title directive does below the hood. Identical with Nuxt’s view transition help. They dynamically assign and take away names across the navigation lifecycle. The frameworks simply conceal the pageswap/pagereveal wiring behind a element attribute. You’re doing the identical factor, simply with out the abstraction layer. Fewer transferring elements, identical consequence.
Sensible Patterns for Actual Content material
The product grid instance covers the most typical case, however let’s run by way of a few different patterns you’ll hit within the wild.
Picture Galleries with Blended Facet Ratios
Galleries are difficult as a result of each thumbnail might need a unique side ratio, and the full-size view positively will. The taffy repair from the Half 1 article is crucial right here, however you additionally need the transition to really feel intentional somewhat than chaotic.
/* Gallery gadgets get their very own class for focused animation */
::view-transition-group(*.gallery-item) {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
::view-transition-old(*.gallery-item),
::view-transition-new(*.gallery-item) {
object-fit: cowl;
overflow: hidden;
}
/* Lightbox-style overlay - fade the background individually */
::view-transition-group(*.lightbox-bg) {
animation-duration: 0.3s;
}
The trick with galleries is assigning the view-transition-name to the itself somewhat than the encircling card or container. You need the browser to morph the picture from thumbnail measurement to lightbox measurement, not the cardboard’s background, padding, and caption together with it. Identify the picture. Fashion the cardboard. Preserve them separate.
For the lightbox background (that darkish overlay), give it its personal view-transition-name and view-transition-class. It’ll fade in independently whereas the picture morphs. Two transitions working in parallel, every with their very own timing. Seems polished, and it’s simply two names.
Tab or Part Transitions Inside a Web page
Not the whole lot is a grid-to-detail sample. Typically you’re transitioning between sections on the identical web page, e.g., dashboard tabs, multi-step types, content material panels. Identical-document view transitions work nice right here, and the view-transition-class method scales the identical manner.
/* Shared header that persists throughout tabs */
::view-transition-group(*.persistent) {
animation-duration: 0s; /* do not animate - it ought to really feel anchored */
}
/* Tab content material that swaps */
::view-transition-group(*.tab-content) {
animation-duration: 0.25s;
}
::view-transition-old(*.tab-content) {
animation: slide-out-left 0.25s ease-in;
}
::view-transition-new(*.tab-content) {
animation: slide-in-right 0.25s ease-out;
}
@keyframes slide-out-left {
to {
rework: translateX(-100%);
opacity: 0;
}
}
@keyframes slide-in-right {
from {
rework: translateX(100%);
opacity: 0;
}
}
The animation-duration: 0s on persistent parts is value calling out. In case your web site header has a view-transition-name (so it stays in place as an alternative of collaborating within the default root cross-fade), you most likely don’t need it animating in any respect. Zero-duration makes it snap to its new place immediately, which feels prefer it by no means moved. That’s the purpose — secure landmarks make the transitioning content material really feel grounded.
Dynamic Content material and Infinite Scroll
Right here’s a sample that catches folks off guard. You’ve acquired a product grid with infinite scroll, loading new gadgets because the consumer scrolls down. Every new batch arrives by way of fetch() and will get appended to the DOM. Do these new gadgets want view-transition-name?
No. Not till somebody clicks one.
With the just-in-time sample, it doesn’t matter whether or not a component existed at web page load or was added dynamically 5 minutes later. The pageswap handler queries the DOM for the time being of navigation. If the ingredient is there, it finds it, names it, achieved. Your infinite scroll gadgets work identically to your preliminary web page load gadgets with none further setup.
The one factor to be careful for: be sure your data-id attributes (or no matter you’re utilizing to match parts) are distinctive throughout all loaded batches. In case your API returns gadgets with IDs and also you’re utilizing these for the view-transition-name, you’re already superb. When you’re producing IDs client-side, be sure they don’t collide when new batches load.
Don’t Make Folks Sick
/* The accountable solution to arrange view transitions */
@view-transition {
navigation: auto;
}
/* All of your animation customizations go INSIDE this media question */
@media (prefers-reduced-motion: no-preference) {
::view-transition-group(*.card) {
animation-duration: 0.35s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(*.card),
::view-transition-new(*.card) {
object-fit: cowl;
}
/* Customized keyframes, staggered delays, the enjoyable stuff - all in right here */
::view-transition-old(root) {
animation: fade-out 0.2s ease-in;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-out;
}
}
/* If the consumer HAS requested diminished movement: on the spot lower, no animation */
@media (prefers-reduced-motion: cut back) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0s !vital;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
}
This isn’t a nice-to-have. I have to be blunt about that.
Folks with vestibular problems — and there are much more of them than most builders notice — can get bodily nauseous from surprising movement on display. Not “mildly aggravated.” Nauseous. Dizzy. Migraines that final hours. The prefers-reduced-motion media question exists as a result of actual folks checked a field of their OS settings that claims “please cease making me sick.” Ignoring it’s the accessibility equal of eradicating a wheelchair ramp as a result of stairs look cleaner.
The @view-transition opt-in can keep exterior the media question. That’s superb, it simply tells the browser, “I would like cross-document transitions enabled.” The browser will nonetheless do an on the spot lower between pages, which is visually equivalent to a traditional navigation. It’s the animation customizations that have to be gated: the durations, the easing curves, the customized keyframes. Wrap all of that in prefers-reduced-motion: no-preference and also you’re coated.
That prefers-reduced-motion: cut back block on the backside is a belt-and-suspenders factor. Even in case you miss wrapping some animation rule, forcing animation-duration: 0s on all of the transition pseudo-elements ensures nothing truly strikes. The !vital is ugly however justified right here. you genuinely need this to override the whole lot, no exceptions.
You already noticed the conditional opt-in sample again in Half 1:
/* You too can simply disable transitions completely for reduced-motion customers */
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
}
Both method works. Wrapping the entire @view-transition rule means the browser received’t even try the transition – it’s a traditional navigation, full cease. Protecting @view-transition lively however killing the animation durations means the transition technically fires however completes immediately, which may matter when you have pagereveal logic that depends upon occasion.viewTransition current. Decide whichever matches your setup. Simply don’t ship animated transitions with out checking.
A factor value contemplating right here: “diminished movement” doesn’t essentially imply “no movement.” Some customers with vestibular sensitivities are superb with fades however not with sliding or zooming. You might supply a gentler different as an alternative of killing all animation completely.
@media (prefers-reduced-motion: cut back) {
/* As a substitute of zero length, use a fast crossfade solely */
::view-transition-group(*) {
animation-duration: 0.15s !vital;
animation-timing-function: linear !vital;
}
::view-transition-old(*) {
animation: fade-out 0.15s linear !vital;
}
::view-transition-new(*) {
animation: fade-in 0.15s linear !vital;
}
}
It is a judgment name. A quick, delicate cross-fade is much less more likely to set off signs than a 400ms morphing animation with easing curves. However the most secure possibility is at all times zero movement, and in case you’re undecided, go together with animation-duration: 0s. You may at all times add a gentler different later when you’ve examined it with precise customers who depend on the setting.
Deal with Outdated Browsers (By Doing Principally Nothing)
/* Characteristic detection, in case you want it */
@helps (view-transition-name: none) {
.card {
/* perhaps you need comprise: paint for higher snapshotting */
comprise: paint;
}
}
// JS-side characteristic detection
if (doc.startViewTransition) {
// same-document transition API exists
}
// For cross-document transitions, there isn't any direct JS verify -
// the browser both helps @view-transition in CSS or ignores it.
// That is... truly superb.
Right here’s the factor although: you most likely don’t even want that @helps verify.
View transitions are progressive enhancement within the purest sense of the time period. If a browser doesn’t perceive @view-transition { navigation: auto; }, it ignores the rule. That’s how CSS works. The consumer clicks a hyperlink, the browser navigates usually, the brand new web page masses. No animation, no morphing, no cross-fade. Only a common web page load. Which is strictly what each web site on the web did for the primary 25 years of the online. It’s superb.
Nothing breaks. No JavaScript errors. No format shifts. No fallback code to put in writing. The view-transition-name properties get ignored. The ::view-transition-* pseudo-element selectors match nothing. Your pageswap and pagereveal occasion listeners both don’t hearth or occasion.viewTransition is null and your guard clause returns early. The entire characteristic is designed to be invisible when it’s absent.
That’s the great thing about this API, truthfully. It’s one of many uncommon internet platform options the place you don’t have to put in writing a single line of fallback code. Firefox doesn’t help it but? High quality — Firefox customers get regular navigation. Safari’s engaged on it however hasn’t shipped? Cool, Safari customers click on hyperlinks and pages load. No person will get an error. No person will get a damaged format. No person loses something. They simply don’t get the flamboyant animation, and most of them won’t ever discover it was presupposed to be there.
Value noting the place issues truly stand as we speak: Chrome and Edge have full help for cross-document view transitions, together with view-transition-class. Safari additionally ships full cross-document help as of Safari 18.2. The momentum is clearly towards common help, regardless that Firefox nonetheless holds it behind a flag for now.
The one time @helps issues is in case you’re including kinds that solely make sense within the context of view transitions — like comprise: paint on parts to enhance snapshot high quality, or hiding some loading state that the transition would usually cowl. Gate these behind @helps (view-transition-name: none) so non-supporting browsers don’t get the unintended effects with out the payoff.
Failure is invisible. That’s the entire level.
Ship It
Look, I’ve been constructing web sites for a very long time, and there’s at all times been this unstated trade-off: you need easy, app-like transitions, you undertake a framework and a client-side router and a construct step and a hydration technique and abruptly you’re sustaining a small plane provider simply so a card can animate right into a hero picture.
That trade-off is dissolving.
Cross-document view transitions let an really feel like a local app navigation. Two HTML recordsdata. Some CSS. Perhaps just a little JavaScript for the flamboyant stuff. The browser does the remainder. That’s not a small factor – it adjustments which initiatives want a framework and which of them simply assumed they did.
The spec is younger. It’s Chromium-only proper now. The tough edges are actual – you’ve seen them throughout each elements of this collection. However the API is designed so nicely that when it’s not supported, nothing breaks. Your web site simply works the way in which websites have at all times labored. And when it is supported, it looks like magic that got here free.
Right here’s a fast cheat sheet to take with you:
- Decide in with CSS, not the deprecated meta tag:
@view-transition { navigation: auto; }. - Each pages should choose in or no transition occurs.
- 4-second timeout begins at navigation, not at render – use
pagerevealto catchTimeoutError. - Pictures stretch throughout transitions as a result of pseudo-elements default to
object-fit: fill– repair it withobject-fit: cowlon::view-transition-oldand::view-transition-new. view-transition-name= id (distinctive per web page),view-transition-class= styling hook (shared throughout parts).- Don’t title parts upfront – use
pageswapandpagerevealto assign names just-in-time. However preserve yourpageswaplogic quick — the browser provides you a slender window (10-50ms) earlier than snapshots. - Clear up names after
viewTransition.completedresolves to keep away from stale conflicts. - Gate animations behind
prefers-reduced-motion: no-preference— this isn’t non-obligatory. - Progressive enhancement is in-built — unsupported browsers simply get regular web page masses
The perfect animations are those you don’t have to take care of a framework to get.
Cross-Doc View Transitions Collection
- The Gotchas No person Mentions
- Scaling View Transitions Throughout A whole lot of Components (You’re right here!)
