Coordinar nombres entre dos documentos
Cómo se emparejan elementos que viven en documentos distintos, el patrón del nombre asignado en pageswap y pagereveal, y qué información se puede pasar.
En una transición de mismo documento, poner el mismo view-transition-name a dos elementos es fácil porque los ves a la vez. Entre documentos, el elemento antiguo y el nuevo viven en mundos separados que no comparten memoria, no comparten variables y solo coinciden en que el navegador los captura con unos milisegundos de diferencia. Emparejarlos exige un protocolo, y el protocolo se reduce a una idea: los dos lados tienen que llegar al mismo nombre a partir de la URL.
- Emparejar un elemento del documento saliente con uno del entrante.
- Derivar un nombre determinista a partir de la URL de destino.
- Elegir el canal correcto para pasar información entre los dos documentos.
- Reconocer qué técnicas de mismo documento dejan de funcionar aquí.
El emparejamiento sigue siendo por nombre
La regla no cambia: dos elementos se emparejan si comparten view-transition-name. Lo que cambia es que ahora uno está en el documento que se va y otro en el que llega, así que hay que garantizar que el nombre coincide sin poder comprobarlo.
Para los elementos persistentes del layout —cabecera, navegación, pie, contenedor principal— es trivial: se declaran en el CSS, que es el mismo en las dos páginas.
header { view-transition-name: vt-cabecera; }
nav.principal{ view-transition-name: vt-nav; }
main { view-transition-name: vt-main; }
footer { view-transition-name: vt-pie; }
El caso interesante es el otro: una miniatura de un listado que debe convertirse en la imagen grande del detalle. Ahí el nombre no puede estar en el CSS, porque en el listado hay veinte miniaturas y solo una participa, y cuál sea depende de dónde haya pulsado el usuario.
El patrón del nombre derivado de la URL
La solución que funciona y que no requiere ningún canal de comunicación: derivar el nombre de la propia URL de destino, que los dos documentos conocen.
En el documento saliente, dentro de pageswap, se busca el enlace cuya URL coincide con el destino y se nombra la imagen que hay dentro:
window.addEventListener('pageswap', (e) => {
if (!e.viewTransition) return;
const destino = e.activation.entry.url;
const enlace = document.querySelector(`a[href="${new URL(destino).pathname}"]`);
if (!enlace) return;
const img = enlace.querySelector('img');
if (img) img.style.viewTransitionName = 'vt-protagonista';
});
En el documento entrante, dentro de pagereveal, se nombra la imagen principal con el mismo identificador:
window.addEventListener('pagereveal', (e) => {
if (!e.viewTransition) return;
const img = document.querySelector('.detalle img');
if (img) img.style.viewTransitionName = 'vt-protagonista';
});
Y ya está. No ha hecho falta pasar ningún dato: el documento saliente sabía a dónde iba y el entrante sabe qué es su imagen principal. El nombre es una constante acordada entre los dos, exactamente igual que el nombre de una clase CSS.
Fíjate en un detalle del primer bloque que ahorra un fallo desagradable: se nombra una sola imagen, la del enlace que corresponde al destino. Si nombraras todas las miniaturas, tendrías veinte elementos con el mismo nombre y la transición se abortaría por duplicado. El error de unicidad es exactamente el mismo aquí que en mismo documento.
El camino de vuelta es simétrico. Al volver del detalle al listado, pageswap corre en el detalle —donde solo hay una imagen, así que es trivial— y pagereveal corre en el listado, donde hay que encontrar la miniatura que corresponde a la URL de la que venimos:
window.addEventListener('pagereveal', (e) => {
if (!e.viewTransition) return;
const origen = navigation.activation.from?.url;
if (!origen) return;
const enlace = document.querySelector(`a[href="${new URL(origen).pathname}"]`);
const img = enlace?.querySelector('img');
if (img) img.style.viewTransitionName = 'vt-protagonista';
});
Los tres fragmentos juntos, repartidos entre las dos plantillas, producen el efecto de la miniatura que crece y vuelve a encogerse. Es el mismo código que escribirías para una aplicación de página única, sin router, sin estado y sin hidratación.
Hay una tentación evidente al implementar esto: guardar en sessionStorage cuál era el elemento pulsado y leerlo al llegar. Funciona en la navegación hacia delante y se rompe en el retroceso, y merece la pena entender por qué, porque el fallo es de los que solo aparecen en manos de usuarios reales. El usuario puede llegar al detalle desde el listado, y desde ahí ir a un tercer sitio, y volver, y volver otra vez. Puede abrir tres pestañas del mismo sitio y navegar en las tres. Puede recargar. Puede llegar al detalle desde un buscador y pulsar “atrás” hacia una página del listado que nunca ha visitado en esta sesión. En todos esos casos, el valor guardado en almacenamiento o bien no existe, o bien es el de otra navegación, y el resultado es una transición que empareja el elemento equivocado: la imagen vuela hacia una miniatura que no es la suya. Derivar el nombre de la URL no tiene ese problema porque la URL siempre está: es el único dato que el navegador garantiza en los dos lados, en cualquier orden de navegación, con cualquier historial y sin depender de que la sesión anterior existiera. La regla general que se deduce, y que va mucho más allá de las transiciones: cuando dos documentos tengan que coordinarse, deriva de la URL todo lo que puedas y guarda estado solo cuando no haya alternativa. La URL es el único estado compartido que el usuario puede reproducir, compartir y guardar en un marcador; todo lo demás es caché, y la caché puede no estar.
Qué información se puede pasar y por dónde
Cuando derivar de la URL no basta, hay tres canales y cada uno tiene su sitio.
La URL, incluidos el fragmento y los parámetros. Es el canal preferente porque sobrevive a todo. Un #producto-42 en el enlace le dice al documento entrante qué elemento era el protagonista, y además hace algo útil por su cuenta: llevar el scroll al sitio correcto.
sessionStorage. Válido para información que de verdad es de sesión y que no tiene sentido en la URL: la posición de scroll de un carrusel, un estado de filtro voluminoso. Nunca para el emparejamiento, por lo dicho arriba.
Los tipos de la transición. Se calculan por separado en cada documento a partir de la información de navegación, así que técnicamente no se “pasan”, pero cumplen la función de un canal: los dos lados llegan a la misma conclusión sobre qué clase de cambio es este.
Lo que no existe es un canal directo entre los dos documentos. No hay un objeto compartido, no puedes guardar una referencia a un elemento del documento antiguo, y no hay forma de ejecutar código en el nuevo desde el viejo. Esa ausencia es deliberada: el documento saliente ya está muerto cuando el entrante se pinta.
Lo que deja de funcionar
Dos técnicas del nivel anterior no sirven aquí y conviene tenerlo presente antes de diseñar.
match-element no puede emparejar. El nombre generado deriva de la identidad del elemento dentro de su documento, y dos documentos distintos nunca producen la misma identidad. Sigue siendo válido escribirlo —cada documento tendrá nombres únicos internos— pero los grupos resultantes no se emparejan entre sí: cada elemento se desvanece por su cuenta. Para emparejar entre documentos hay que dar nombres explícitos.
La asignación de nombres desde una función común es más frágil. En mismo documento, un envoltorio central pone y quita nombres con garantías, porque el mismo código controla los dos extremos. Entre documentos, el código de los dos lados vive en ficheros distintos que se despliegan juntos por convención, no por acoplamiento. La disciplina que evita el desastre es la de siempre: una constante compartida con el nombre, importada por las dos plantillas, en lugar de dos cadenas literales que alguien tendrá que mantener sincronizadas a mano.
// vt-nombres.js, importado por las dos plantillas
export const VT_PROTAGONISTA = 'vt-protagonista';
Parece una exageración para una cadena de quince caracteres. Deja de parecerlo el día que alguien renombre una y no la otra, y el síntoma sea que la transición hace un fundido en lugar de un morphing, sin ningún error en ningún sitio.