El árbol de pseudo-elementos
Los cinco pseudo-elementos que construye el navegador, qué contiene cada uno, dónde vive el árbol y cuáles son las animaciones por defecto.
Todo lo que se puede personalizar de una transición de vista pasa por entender qué árbol construye el navegador y qué hace cada nodo. Son cinco pseudo-elementos en tres niveles, y su diseño no es arbitrario: cada nivel existe porque separa una responsabilidad distinta, y saber cuál te dice exactamente dónde escribir cada declaración cuando quieras cambiar algo.
- Dibujar de memoria el árbol de pseudo-elementos con sus cinco nodos.
- Asignar cada tipo de personalización al nivel correcto del árbol.
- Enumerar las animaciones que aplica el navegador por defecto.
- Explicar por qué el árbol vive en la capa superior y qué implica.
Los cinco nodos
El árbol cuelga del elemento raíz del documento y tiene esta forma:
::view-transition
└── ::view-transition-group(nombre)
└── ::view-transition-image-pair(nombre)
├── ::view-transition-old(nombre)
└── ::view-transition-new(nombre)
Hay un grupo por cada nombre capturado, así que el nivel de group se repite tantas veces como elementos con view-transition-name haya. Vamos nodo por nodo, porque la responsabilidad de cada uno es lo que decide dónde va cada declaración.
::view-transition es la raíz. Ocupa el bloque contenedor de instantáneas —el viewport menos la interfaz fija del navegador— y se comporta como el lienzo sobre el que se dibuja todo. Aquí van las declaraciones que afectan a la transición entera: un fondo, un filtro global, un pointer-events.
::view-transition-group(nombre) representa la caja de ese elemento. Es el nodo cuya geometría se anima: el navegador le aplica una animación que va del tamaño y la posición que el elemento tenía en el estado antiguo a los que tiene en el nuevo. Cuando alguien dice que un elemento “hace morphing” de un sitio a otro, lo que está pasando es que este nodo se está moviendo y redimensionando.
::view-transition-image-pair(nombre) es un contenedor con aislamiento de mezcla. Su única razón de existir es que las dos imágenes de dentro puedan combinarse entre ellas sin afectar al resto de la página. Rara vez se estiliza directamente, y cuando se hace suele ser para cambiar el modo de mezcla.
::view-transition-old(nombre) contiene la captura del estado antiguo. Es una imagen estática: un fotograma congelado de cómo se veía ese trozo de interfaz antes de la mutación.
::view-transition-new(nombre) contiene el estado nuevo, como representación en vivo del elemento actual.
Esa asimetría entre “imagen estática” y “representación en vivo” es deliberada y tiene consecuencias que veremos en la última lección del nivel: lo viejo ya no existe y solo puede ser una foto; lo nuevo existe y puede seguir vivo.
Qué anima el navegador por defecto
La hoja de estilos del navegador aplica animaciones a tres de los cinco nodos, con una duración de un cuarto de segundo y animation-fill-mode: both:
- Sobre
::view-transition-group(...): una animación de la geometría, que interpolawidth,heightytransformdesde los valores del estado antiguo a los del nuevo. - Sobre
::view-transition-old(...): un desvanecido de salida. - Sobre
::view-transition-new(...): un desvanecido de entrada.
Las dos últimas juntas producen el fundido cruzado. Combinadas con la primera, producen el efecto de que un elemento se transforma en otro mientras se mueve.
Hay un grupo que existe siempre aunque no hayas puesto ningún nombre: el navegador declara view-transition-name: root sobre el elemento raíz en su hoja de estilos, así que el documento entero es un elemento capturado y siempre hay un ::view-transition-group(root). Por eso una transición sin ningún nombre propio ya produce un fundido: es el grupo root haciendo su trabajo.
Sobrescribir cualquiera de esas animaciones es CSS normal:
/* Un fundido mas lento para toda la pagina */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 500ms;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
Y desactivarlas también:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}
Ese asterisco es un comodín que casa con cualquier nombre y es la forma correcta de aplicar algo a todos los grupos sin enumerarlos. Fíjate en que desactivar la animación no desactiva la transición: el cambio sigue siendo atómico, sigue sin verse ningún estado intermedio, y simplemente ocurre de golpe. Es exactamente lo que quiere quien ha pedido reducir el movimiento.
El árbol entero se pinta en la capa superior, la misma donde viven los diálogos modales y los popovers, y eso rompe tres intuiciones a la vez. Primera: el orden de apilamiento entre grupos no lo controlas con z-index sobre tus elementos originales, porque los originales dejan de pintarse durante la transición; el orden entre grupos lo decide el navegador a partir del orden en el árbol y del z-index que apliques al pseudo-elemento group, que es un sitio distinto y menos obvio. Segunda: cualquier overflow: hidden, clip-path o contexto de apilamiento de un ancestro deja de aplicar durante la transición, porque la captura ya no está dentro de ese ancestro. Es la explicación de un fenómeno que desconcierta mucho: un elemento que estaba pulcramente recortado dentro de una tarjeta con esquinas redondeadas se sale del recorte en cuanto empieza la transición, y el arreglo no es tocar la tarjeta sino aplicar el recorte al grupo. Tercera: los grupos se posicionan respecto al bloque contenedor de instantáneas, que es el viewport menos la interfaz del navegador, no respecto a tu layout; por eso un elemento con position: fixed y un elemento en flujo se comportan igual dentro de la transición, y por eso las coordenadas que ves en el inspector no cuadran con las de tu documento. La regla operativa que se deriva de las tres: durante una transición, tu jerarquía de layout no existe; existe una lista plana de grupos sobre un lienzo. Cuando algo se ve mal apilado o mal recortado, deja de mirar el CSS de tus elementos y mira el de los pseudo-elementos.
Dónde va cada personalización
La utilidad de conocer los cinco nodos es saber en cuál escribir. Esta es la correspondencia completa:
| Lo que quieres | Nodo |
|---|---|
| Cambiar la duración o la curva del movimiento del elemento | group |
| Mover el elemento por una trayectoria distinta | group |
| Recortar el elemento durante la transición | group |
| Cambiar el orden de apilamiento entre elementos | group, con z-index |
| Que lo viejo salga de otra forma | old |
| Que lo nuevo entre de otra forma | new |
| Un fundido sin movimiento | old y new, y animation: none en group |
| Un movimiento sin fundido | group, y animation: none en old y new |
| Combinar las dos imágenes con otro modo de mezcla | image-pair |
| Un fondo o un filtro para toda la transición | ::view-transition |
Los dos casos del medio son los que más se usan y merecen verse escritos:
/* Movimiento sin fundido: util cuando los dos estados son el mismo contenido */
::view-transition-old(cabecera),
::view-transition-new(cabecera) {
animation: none;
mix-blend-mode: normal;
}
/* Fundido sin movimiento: util cuando el contenido cambia pero la caja no */
::view-transition-group(panel) {
animation: none;
}
Ese mix-blend-mode: normal del primer bloque no es decorativo: sin él, las dos imágenes se siguen combinando con el modo que aplica el navegador para el fundido cruzado, y como ninguna de las dos se desvanece, el resultado es una superposición turbia. Cuando quites las animaciones de old y new, quita también la mezcla.
Selectores por nombre y comodines
Los cuatro pseudo-elementos que llevan nombre aceptan un identificador concreto o el comodín *:
::view-transition-group(foto) /* solo el grupo llamado foto */
::view-transition-group(*) /* todos los grupos */
El comodín es cómodo y tiene un peligro que conviene anticipar: casa también con root, es decir, con la página entera. Un ::view-transition-group(*) { animation-duration: 1s } ralentiza también el fundido del documento completo, que casi nunca es lo que se quiere. Cuando necesites “todos menos la raíz”, la vía limpia no es una lista de exclusiones sino agrupar los elementos con view-transition-class, que es el tema de una lección del nivel siguiente.