In Half 1, we lined 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 develop 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 aspect 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 have to morph right into a element view, and also you understand the tutorials ignored the exhausting 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 Lots of of Parts (You might be right here!)
The Dream: One Line, Infinite Names
In an ideal world, you’d clear up the scaling drawback with pure CSS. No JavaScript. No server-side loops. Simply this:
.card {
/* Generates card-1, card-2, card-3, and so forth. robotically */
view-transition-name: ident("card-" sibling-index());
}
That’s ident() — a CSS perform 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 aspect 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 may 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 should use it at the moment for issues like staggered animations and calculated kinds. The lacking piece is ident(). There’s a Chrome Intent to Prototype from Might 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. But it surely’s price realizing 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 clear up the identical drawback effectively at the moment — with the instruments that really exist in browsers proper now.
100 Merchandise, 100 Names, 1 Nightmare
Right here’s what occurs whenever 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 should you comply with the tutorials that solely present one or two named components. They assign view-transition-name: hero to at least one picture and name it a day. Cool. Now strive constructing a product grid.
Each view-transition-name on a web page have to be distinctive. That’s a tough rule — if two components share a reputation, the browser doesn’t know which one maps to which on the subsequent web page, so it throws the entire transition out. On an inventory 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 may 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 might want to perceive the distinction between two properties that sound like they do the identical factor however completely don’t.
Title vs. Class: The Distinction That Adjustments The whole lot
And yeah, the naming right here is complicated. I’ll be sincere: the primary time I noticed view-transition-name and view-transition-class subsequent to one another, I believed they had been interchangeable. They’re not, and the distinction issues.
Title = identification. It solutions: “Which aspect on Web page A is the similar aspect 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 have to be distinctive per web page. Two components can’t each be card-7 or the entire thing breaks.
Class = styling hook. It solutions: “How ought to the animation look?” When fifty components all have view-transition-class: card, you may write one CSS rule that controls the period, easing, and object-fit for all of them. It’s the identical psychological mannequin as CSS courses on common components — .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 may run a question throughout all of them directly.
Right here’s what that appears like in follow:
There it’s. Six playing cards, six distinctive names, however precisely three CSS guidelines dealing with all of the animation habits. May very well be sixty playing cards. May very well 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 aspect has view-transition-class: card, no matter what its particular title is.”
For cross-document multi-page utility (MPA) transitions, the sample is identical 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 whole animation stylesheet for a website with hundreds of merchandise. Three guidelines. Irrespective 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 had 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 attainable title permutation at construct time. It labored, technically, the identical means 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 correct means.
One gotcha: view-transition-class was added to the spec later to repair these precise scaling points. The property landed in Chrome 125 and is now in Chrome, Edge, and Safari 18.2+. Older Chromium variations and Firefox gained’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 may as well assign a number of courses to a single aspect, identical to common CSS courses. One thing like view-transition-class: card featured is legitimate, and you’ll goal it with both ::view-transition-group(*.card) or ::view-transition-group(*.featured). Helpful whenever you need most merchandise to transition the identical means however want a number of to face out with a distinct animation type.
Don’t Title The whole lot Upfront
The whole lot 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. But it surely 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 person 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 aspect 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 components being individually captured, diffed, and animated when the person 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 cellphone loading a grid of product pictures over LTE? You’ll really feel it. The transition stutters or the browser simply skips it fully as a result of it could possibly’t set all the pieces up quick sufficient.
The repair is to deal with view-transition-name like a just-in-time factor. Assign it in the mean time of interplay, not at web page load.
The lifecycle goes like this:
- Consumer 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 person goes, finds the clicked card, slapsview-transition-name: product-42on it. - Browser snapshots that one named aspect (plus the default
roottransition). - Navigation occurs, new web page masses.
pagerevealfires on the incoming web page.- Your
pagerevealhandler reads the URL, finds the hero aspect, 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 aspect named, one aspect transitioned, zero waste.
The occasion.activation object is your greatest pal 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 all the pieces you might want to determine which aspect 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 person navigates again to the itemizing web page and clicks a totally 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 demise) or wrong-element matching (the brand new web page morphs from the incorrect card). Clear up after your self.
This sample is mainly what Astro’s transition:title directive does below the hood. Similar with Nuxt’s view transition assist. They dynamically assign and take away names across the navigation lifecycle. The frameworks simply disguise the pageswap/pagereveal wiring behind a part attribute. You’re doing the identical factor, simply with out the abstraction layer. Fewer transferring elements, similar consequence.
Sensible Patterns for Actual Content material
The product grid instance covers the commonest case, however let’s run by way of a few different patterns you’ll hit within the wild.
Photograph Galleries with Combined Facet Ratios
Galleries are tough as a result of each thumbnail might need a distinct side ratio, and the full-size view undoubtedly will. The taffy repair from the Half 1 article is important right here, however you additionally need the transition to really feel intentional fairly 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 fairly than the encompassing card or container. You need the browser to morph the picture from thumbnail dimension to lightbox dimension, not the cardboard’s background, padding, and caption together with it. Title the picture. Type the cardboard. Maintain 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 operating in parallel, every with their very own timing. Appears polished, and it’s simply two names.
Tab or Part Transitions Inside a Web page
Not all the pieces is a grid-to-detail sample. Typically you’re transitioning between sections on the identical web page, e.g., dashboard tabs, multi-step kinds, content material panels. Similar-document view transitions work nice right here, and the view-transition-class method scales the identical means.
/* 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 components is price calling out. In case your website header has a view-transition-name (so it stays in place as an alternative of taking part 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 bought a product grid with infinite scroll, loading new gadgets because the person scrolls down. Every new batch arrives through 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 in the mean time of navigation. If the aspect is there, it finds it, names it, finished. Your infinite scroll gadgets work identically to your preliminary web page load gadgets with none additional setup.
The one factor to be careful for: make certain your data-id attributes (or no matter you’re utilizing to match components) 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 tremendous. Should you’re producing IDs client-side, make certain they don’t collide when new batches load.
Don’t Make Individuals Sick
/* The accountable technique 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 person HAS requested lowered movement: on the spot lower, no animation */
@media (prefers-reduced-motion: scale back) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0s !necessary;
}
}
@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.
Individuals with vestibular issues — and there are much more of them than most builders understand — can get bodily nauseous from sudden movement on display screen. Not “mildly irritated.” 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 outdoors the media question. That’s tremendous, it simply tells the browser, “I need cross-document transitions enabled.” The browser will nonetheless do an on the spot lower between pages, which is visually equivalent to a standard 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 lined.
That prefers-reduced-motion: scale back block on the backside is a belt-and-suspenders factor. Even should you miss wrapping some animation rule, forcing animation-duration: 0s on all of the transition pseudo-elements ensures nothing really strikes. The !necessary is ugly however justified right here. you genuinely need this to override all the pieces, no exceptions.
You already noticed the conditional opt-in sample again in Half 1:
/* You may as well simply disable transitions fully 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 gained’t even try the transition – it’s a standard navigation, full cease. Conserving @view-transition lively however killing the animation durations means the transition technically fires however completes immediately, which might matter when you’ve got pagereveal logic that is determined by occasion.viewTransition current. Choose whichever suits your setup. Simply don’t ship animated transitions with out checking.
A factor price contemplating right here: “lowered movement” doesn’t essentially imply “no movement.” Some customers with vestibular sensitivities are tremendous with fades however not with sliding or zooming. You may provide a gentler various as an alternative of killing all animation fully.
@media (prefers-reduced-motion: scale back) {
/* As a substitute of zero period, use a fast crossfade solely */
::view-transition-group(*) {
animation-duration: 0.15s !necessary;
animation-timing-function: linear !necessary;
}
::view-transition-old(*) {
animation: fade-out 0.15s linear !necessary;
}
::view-transition-new(*) {
animation: fade-in 0.15s linear !necessary;
}
}
It is a judgment name. A quick, delicate cross-fade is much less prone to set off signs than a 400ms morphing animation with easing curves. However the most secure choice is all the time zero movement, and should you’re undecided, go together with animation-duration: 0s. You’ll be able to all the time add a gentler various later when you’ve examined it with precise customers who depend on the setting.
Deal with Outdated Browsers (By Doing Mainly Nothing)
/* Function detection, should you want it */
@helps (view-transition-name: none) {
.card {
/* perhaps you need comprise: paint for higher snapshotting */
comprise: paint;
}
}
// JS-side function detection
if (doc.startViewTransition) {
// same-document transition API exists
}
// For cross-document transitions, there is not any direct JS verify -
// the browser both helps @view-transition in CSS or ignores it.
// That is... really tremendous.
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 person 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 net. It’s tremendous.
Nothing breaks. No JavaScript errors. No structure 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 function is designed to be invisible when it’s absent.
That’s the fantastic thing about this API, actually. It’s one of many uncommon net platform options the place you don’t have to put in writing a single line of fallback code. Firefox doesn’t assist it but? Nice — 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 structure. No person loses something. They only don’t get the flowery animation, and most of them won’t ever discover it was presupposed to be there.
Value noting the place issues really stand at the moment: Chrome and Edge have full assist for cross-document view transitions, together with view-transition-class. Safari additionally ships full cross-document assist as of Safari 18.2. The momentum is clearly towards common assist, despite the fact that Firefox nonetheless holds it behind a flag for now.
The one time @helps issues is should you’re including kinds that solely make sense within the context of view transitions — like comprise: paint on components 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 negative 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 all the time been this unstated trade-off: you need clean, app-like transitions, you undertake a framework and a client-side router and a construct step and a hydration technique and all of the sudden 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 a bit of JavaScript for the flowery stuff. The browser does the remainder. That’s not a small factor – it adjustments which tasks 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 effectively that when it’s not supported, nothing breaks. Your website simply works the way in which websites have all the time labored. And when it is supported, it seems like magic that got here free.
Right here’s a fast cheat sheet to take with you:
- Choose in with CSS, not the deprecated meta tag:
@view-transition { navigation: auto; }. - Each pages should decide 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= identification (distinctive per web page),view-transition-class= styling hook (shared throughout components).- Don’t title components upfront – use
pageswapandpagerevealto assign names just-in-time. However preserve yourpageswaplogic quick — the browser offers 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-compulsory. - Progressive enhancement is inbuilt — unsupported browsers simply get regular web page masses
The perfect animations are those you don’t have to keep up a framework to get.
Cross-Doc View Transitions Collection
- The Gotchas No person Mentions
- Scaling View Transitions Throughout Lots of of Parts (You might be right here!)









