Egy nézetváltási animáció SPA-ban sokáig úgy készült, hogy az ember hazudott a felhasználónak. A régi DOM-ot életben tartotta, abszolút pozícióba tette, getBoundingClientRect()-tel kimérte, hova kell csúsznia, FLIP-technikával visszatranszformálta, majd imádkozott, hogy közben senki ne resizolja az ablakot. Aki írt már ilyet, az pontosan tudja, miért lett a Floating UI-ból és társaiból iparág.
A View Transitions API-val ez papíron egy sor: document.startViewTransition(callback), és a böngésző szépen áttűnteti a régi állapotot az újba. A demók látványosak, a lelkesedés érthető. Amit viszont a demók nem mondanak el: a böngésző nem a DOM-odat animálja, hanem lefényképezi. És ebből az egyetlen mondatból következik szinte az összes meglepetés, amibe éles használatnál belefutsz — a kattinthatatlan oldaltól a némán kihagyott animációig.

Screenshot-színház
A startViewTransition() hívásakor a következő történik: a böngésző képet készít az oldal aktuális állapotáról, felfüggeszti a renderelést, lefuttatja a callbackedet (ebben cseréled a DOM-ot), majd képet készít az új állapotról is. Ezután a két állapotot egy pszeudo-elem fába teszi, amit a valódi tartalom fölé renderel:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root) ← statikus kép a régi állapotról
└─ ::view-transition-new(root) ← élő nézet az új állapotról
A kettő nem szimmetrikus, és ez fontos. A ::view-transition-old tényleg egy pillanatfelvétel: ha az eltűnő nézetben videó ment, az a snapshoton megfagy. A ::view-transition-new viszont a specifikáció szavaival „élő reprezentáció”: ha az új tartalomban pörög egy spinner, az az áttűnés alatt is pörög. Az alapértelmezett animáció egy crossfade a kettő között, és ha egy elemnek saját view-transition-name-et adsz, az kiemelkedik a saját ::view-transition-group-jába, a böngésző pedig a pozícióját és a méretét is interpolálja a két állapot között. Ezek egyébként teljesen hétköznapi CSS-animációk: a DevToolsban megnézheted, lassíthatod, felülírhatod őket.
const vt = document.startViewTransition(() => {
render(nextState); // csak DOM-csere, semmi más
});
vt.ready.catch(() => {
// az animáció elmaradt – a DOM attól még frissült
});
Eddig a szép rész. Most jöjjön az, amiért ez a cikk készült.
Az oldal, amire nem lehet kattintani
Amíg az átmenet fut, a felhasználó nem az oldaladat látja, hanem a fölé rakott pszeudo-elemeket. A valódi, nevesített elemek addig nem renderelődnek — és ami ennél rosszabb: a ::view-transition overlay a teljes viewportot lefedi, és elkapja az összes kattintást. Egy 200 ms-os crossfade-nél ezt senki nem veszi észre. Egy 1,5 másodperces, gondosan koreografált hero-animációnál viszont a felhasználó rákattint a menüre, és nem történik semmi.
A javítás egyetlen sor, amit Bramus Van Damme snippetje óta fél internet másol:
::view-transition {
pointer-events: none;
}
Hogy ez miért nem alapértelmezés, arról a CSS munkacsoportban is folyt vita. A pszeudo-elemekre eseménykezelőt kötni amúgy sem tudsz, a kattintás így a mögöttes — már az új állapotot mutató — DOM-ra esik, ami az esetek túlnyomó részében pontosan az, amit akarsz.
Két elem, egy név: néma skip
A view-transition-name-nek a snapshot pillanatában egyedinek kell lennie az oldalon. Ha két renderelt elem ugyanazt a nevet viseli — klasszikus eset: egy listakomponens, ahol mindenki view-transition-name: card-ot kapott —, a böngésző nem próbál okoskodni, hanem kihagyja a teljes átmenetet, és a ready promise InvalidStateError-rel utasítódik el. A DOM frissül, animáció nincs, hibaüzenet legfeljebb a konzolon, unhandled rejection formájában — vagyis az a fajta hiba, amit staging környezetben két hétig nem vesz észre senki, mert „csak” az animáció tűnt el.
Listáknál ezért a nevet az adatból kell képezni — style="view-transition-name: vt-item-42" —, ami pont olyan kényelmetlen, mint ahogy hangzik. A Level 2 specifikáció erre hozza az auto és a match-element kulcsszavakat, hogy a böngésző generáljon egyedi neveket, de a támogatásuk még mozgó célpont — ellenőrizd, mielőtt ráépítesz.
Fagyott renderelés, négy másodperces türelem
A callback futása alatt a renderelés áll. Nem lassú: áll. Async callbacknél a böngésző megvárja a visszaadott promise-t is — közben a felhasználó egy befagyott oldalt bámul. A Chrome dokumentációja ezért mondja ki: a callbackben ne csinálj semmit, csak DOM-cserét. Ha mégis megpróbálsz benne fetch-elni, a Chrome nagyjából négy másodperc után megunja, TimeoutError-rel kihagyja az átmenetet, és mehet a hibakeresés. Az adat legyen meg, mielőtt a startViewTransition()-t meghívod — az átmenet a már kész állapotok közti vizuális híd, nem loading-screen.
Ugyanez oldalak között
A mechanizmus dokumentumok közötti navigációra is működik: a régi oldal snapshotja túléli a navigációt, és az új oldalon úszik át. Nem kell hozzá JavaScript, csak egy kölcsönös opt-in mindkét érintett oldal CSS-ében:
@view-transition {
navigation: auto;
}
Ez csak same-origin navigációra megy, finomhangolni a pageswap és pagereveal eseményekben lehet, és a négy másodperces szabály itt is él: ha a navigáció ennyi alatt nem ér célba, az átmenet elmarad. Lassú szerverrel tehát nem látványos áttűnést kapsz, hanem semmit — hacsak nem kombinálod prerenderrel, amivel viszont kifejezetten jól működik együtt.
A támogatás itt válik ketté. A same-document átmenet ma már mindenhol megvan: Chrome 111, Safari 18, és a Firefox 144 óta a sor teljes. A cross-document viszont Chrome 126-tól és Safari 18.2-től él, Firefoxban pedig még nyitott ticket.
Használnám? Igen — de nem úgy, ahogy a demók
Az API-nak van egy ritka erénye: természeténél fogva progressive enhancement. Ha a startViewTransition nem létezik, a fallback nem egy elcsúszott layout, hanem az, ami eddig is volt: animáció nélküli állapotváltás. Egy if (!document.startViewTransition) { render(nextState); return; } ág, és kész. Emiatt same-document átmenetre production-ben ma már nyugodt szívvel mondok igent: lista-átrendezés, téma-váltás, egy kép „kinagyítása” részletnézetbe — pár sor CSS, és olyan hatást kelt, amiért korábban egy animációs library-t kellett behúzni.
Amire nemet mondanék: a minden route-váltásra rákényszerített, többszereplős koreográfia. A pszeudo-elem fa debugolása türelemjáték, a duplikált nevek futásidőben, némán ütnek vissza, és minden egyes nevesített elem karbantartandó csatolást jelent két, egymásról nem tudó nézet között. A prefers-reduced-motion tisztelete pedig nem opció, hanem alapkövetelmény — a crossfade-et is kapcsold ki, ha a felhasználó azt kérte.
A lényeg, amit érdemes megjegyezni: a View Transitions API nem a DOM-odat animálja, hanem két képet — egy megfagyott múltat és egy élő jelent. Aki ezt fejben tartja, annak a gotchák nagy része nem meglepetés, hanem logikus következmény.