View Transitions
Lección 32 de 37 · 4 min
Pasar de una vista a otra suele ser un corte seco. Con View Transitions el navegador anima el cambio por vos: le decís qué cambia y él se encarga del movimiento.
Captura, cambio, animación
Una view transition funciona en tres pasos:
- Captura
foto viejaEl navegador saca una foto del estado actual de la página. - Cambio
tu funciónCorre tu función, que actualiza el DOM. - Animación
::view-transitionPone las fotos viejas y la página nueva en una capa por encima, y anima de unas a otra.
Dentro de una misma página, la transición arranca con document.startViewTransition(), que recibe la función que cambia el DOM. Es Baseline recién disponible desde octubre de 2025: anda en las versiones actuales de Chrome, Edge, Safari y Firefox. Donde no existe, cambiá el DOM igual, sin animación:
function cambiarVista(actualizar) {if (!document.startViewTransition) {actualizar();return;}document.startViewTransition(actualizar);}
Sin más CSS, la animación por defecto es un fundido (crossfade) de 250ms: la foto vieja se desvanece mientras la nueva aparece. Elegí un estilo, ajustá la duración y tocá una familia tipográfica para abrir su muestra; Volver la cierra. En morph, el «Ag» viaja de su fila a la muestra grande: la línea punteada marca el elemento con nombre propio. El demo además mueve todo con cubic-bezier(0.22, 1, 0.36, 1), una curva que arranca rápido y frena suave.
Cada sistema resuelve estas palabras clave con sus propias fuentes, así que en otro equipo las muestras pueden verse distintas.
@supports (view-transition-name: none) {.escenario {view-transition-name: escenario;}::view-transition-group(*) {animation-duration: 300ms;animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);}}@media (prefers-reduced-motion: reduce) {::view-transition-group(*),::view-transition-old(*),::view-transition-new(*) {animation: none !important;}}
<html class="[&::view-transition-group(*)]:[animation-duration:300ms] [&::view-transition-group(*)]:[animation-timing-function:cubic-bezier(0.22,1,0.36,1)] motion-reduce:[&::view-transition-group(*)]:animate-none"><div class="[view-transition-name:escenario]">…</div></html>
Nombres y pseudo-elementos
view-transition-name le da a un elemento su propia captura. Si un elemento con el mismo nombre existe antes y después del cambio, el navegador no lo funde: hace un morph, y lo lleva de su posición y tamaño viejos a los nuevos. El nombre tiene que ser único en toda la página; si dos elementos visibles lo comparten, el navegador saltea la transición. Con las capturas arma un árbol de pseudo-elementos que podés estilar como cualquier otro:
- La capa
::view-transitionCubre toda la página mientras dura la transición. - El grupo
::view-transition-group(nombre)Mueve y redimensiona. Acá van la duración y la curva. - El par
::view-transition-image-pair(nombre)Junta las dos capturas. Se dibujan sobre toda la página, así que eloverflow: hiddendel contenedor no las recorta: para que un deslizamiento no se salga, ponéoverflow: clipacá. - Vieja y nueva
::view-transition-old · -newLa captura vieja y la nueva. Cambiá su animación con tus propios@keyframes, como hace el deslizamiento del demo.
view-transition-class agrupa varios nombres bajo una clase, así un solo selector los estila a todos. También es Baseline recién disponible desde 2025:
.familia-1 .muestra {view-transition-name: familia-1-muestra;view-transition-class: familia;}::view-transition-group(*.familia) {animation-duration: 400ms;}
Toda la página participa de la transición: cada elemento con nombre se captura, cambie o no. Poné los nombres solo en lo que cambia y sacalos cuando la transición termina, como hace el demo.
Entre páginas y con movimiento reducido
En un sitio de varias páginas no hay función que llamar: la página de origen y la de destino optan con @view-transition, y tienen que ser del mismo origen. Su soporte todavía es limitado (Chrome y Edge 126, Safari 18.2, Firefox no), así que usalo como mejora progresiva: los navegadores que no lo entienden ignoran la regla y navegan como siempre.
@view-transition {navigation: auto;}
En frameworks, la integración pasa por el componente: Next.js usa el <ViewTransition> de React (incluido en React 19.3), y las navegaciones del App Router lo activan. Por debajo es la misma API, así que los mismos pseudo-elementos sirven para estilarla.
Respetá prefers-reduced-motion: si el sistema pide menos movimiento, cambiá el DOM sin llamar a startViewTransition(), como hace el demo, o apagá las animaciones con CSS:
@media (prefers-reduced-motion: reduce) {::view-transition-group(*),::view-transition-old(*),::view-transition-new(*) {animation: none !important;}}
En Tailwind
Tailwind v4 no trae utilidades para View Transitions. Los nombres van en propiedades arbitrarias, y como los pseudo-elementos ::view-transition-* cuelgan de la raíz del documento, sus reglas van en <html> con una variante arbitraria.
| CSS | Tailwind |
|---|---|
| view-transition-name: familia-1-muestra | [view-transition-name:familia-1-muestra] |
| view-transition-class: familia | [view-transition-class:familia] |
| ::view-transition-group(*) con animation-duration: 300ms, en <html> | [&::view-transition-group(*)]:[animation-duration:300ms] |
| @media (prefers-reduced-motion: reduce) | motion-reduce:[&::view-transition-group(*)]:animate-none |
<html class="[&::view-transition-group(*)]:[animation-duration:300ms]motion-reduce:[&::view-transition-group(*)]:animate-none"><body><div class="[view-transition-name:familia-1-muestra][view-transition-class:familia]">…</div></body></html>
View Transitions
Lesson 32 of 37 · 4 min
Going from one view to another is usually a hard cut. With View Transitions the browser animates the change for you: you tell it what changes and it handles the motion.
Capture, change, animate
A view transition works in three steps:
- Capture
old snapshotThe browser takes a snapshot of the page's current state. - Change
your functionYour function runs and updates the DOM. - Animate
::view-transitionThe old snapshots and the new page go in a layer on top, and the browser animates from one to the other.
Within a single page, the transition starts with document.startViewTransition(), which takes the function that changes the DOM. It's Baseline newly available since October 2025: it works in current versions of Chrome, Edge, Safari and Firefox. Where it doesn't exist, change the DOM anyway, without animation:
function changeView(update) {if (!document.startViewTransition) {update();return;}document.startViewTransition(update);}
With no extra CSS, the default animation is a 250ms crossfade: the old snapshot fades out while the new one fades in. Pick a style, set the duration and press a type family to open its specimen; Back closes it. In morph, the “Ag” travels from its row to the large specimen: the dashed line marks the element with its own name. The demo also moves everything on cubic-bezier(0.22, 1, 0.36, 1), a curve that starts fast and settles gently.
Each system maps these keywords to its own fonts, so the samples may look different on another device.
@supports (view-transition-name: none) {.stage {view-transition-name: stage;}::view-transition-group(*) {animation-duration: 300ms;animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);}}@media (prefers-reduced-motion: reduce) {::view-transition-group(*),::view-transition-old(*),::view-transition-new(*) {animation: none !important;}}
<html class="[&::view-transition-group(*)]:[animation-duration:300ms] [&::view-transition-group(*)]:[animation-timing-function:cubic-bezier(0.22,1,0.36,1)] motion-reduce:[&::view-transition-group(*)]:animate-none"><div class="[view-transition-name:stage]">…</div></html>
Names and pseudo-elements
view-transition-name gives an element its own snapshot. If an element with the same name exists before and after the change, the browser doesn't fade it: it morphs it from its old position and size to the new ones. The name must be unique in the whole page; if two visible elements share it, the browser skips the transition. From the snapshots it builds a tree of pseudo-elements you can style like any other:
- The layer
::view-transitionCovers the whole page while the transition runs. - The group
::view-transition-group(name)Moves and resizes. The duration and the easing go here. - The pair
::view-transition-image-pair(name)Holds the two snapshots. They're drawn over the whole page, so the container'soverflow: hiddendoesn't clip them: to keep a slide inside, putoverflow: cliphere. - Old and new
::view-transition-old · -newThe old and the new snapshot. Change their animation with your own@keyframes, as the demo's slide does.
view-transition-class groups several names under one class, so a single selector styles them all. It's also Baseline newly available since 2025:
.family-1 .sample {view-transition-name: family-1-sample;view-transition-class: family;}::view-transition-group(*.family) {animation-duration: 400ms;}
The whole page takes part in the transition: every named element gets captured, whether it changes or not. Put names only on what changes and remove them when the transition ends, as the demo does.
Between pages and with reduced motion
On a multi-page site there's no function to call: the page you leave and the page you land on opt in with @view-transition, and they must share the same origin. Support is still limited (Chrome and Edge 126, Safari 18.2, not Firefox), so use it as progressive enhancement: browsers that don't understand the rule ignore it and navigate as usual.
@view-transition {navigation: auto;}
In frameworks, the integration goes through a component: Next.js uses React's <ViewTransition> (included in React 19.3), and App Router navigations trigger it. Underneath it's the same API, so the same pseudo-elements style it.
Honor prefers-reduced-motion: when the system asks for less motion, change the DOM without calling startViewTransition(), as the demo does, or turn the animations off with CSS:
@media (prefers-reduced-motion: reduce) {::view-transition-group(*),::view-transition-old(*),::view-transition-new(*) {animation: none !important;}}
In Tailwind
Tailwind v4 has no View Transitions utilities. Names go in arbitrary properties, and since the ::view-transition-* pseudo-elements hang off the document root, their rules go on <html> through an arbitrary variant.
| CSS | Tailwind |
|---|---|
| view-transition-name: family-1-sample | [view-transition-name:family-1-sample] |
| view-transition-class: family | [view-transition-class:family] |
| ::view-transition-group(*) with animation-duration: 300ms, on <html> | [&::view-transition-group(*)]:[animation-duration:300ms] |
| @media (prefers-reduced-motion: reduce) | motion-reduce:[&::view-transition-group(*)]:animate-none |
<html class="[&::view-transition-group(*)]:[animation-duration:300ms]motion-reduce:[&::view-transition-group(*)]:animate-none"><body><div class="[view-transition-name:family-1-sample][view-transition-class:family]">…</div></body></html>