wandres.dev
VIEW TRANSITIONS II · Nombres, grupos y personalización

view-transition-name y la restricción de unicidad

La regla que hace fallar más transiciones que ninguna otra, cómo se manifiesta el fallo, y las tres formas de garantizar nombres únicos.

⏱ 18 min

Antes de cualquier otra cosa sobre nombres, esta: dos elementos renderizados con el mismo view-transition-name en el mismo momento abortan la transición entera. No la degradan, no capturan uno de los dos, no avisan en la consola con un mensaje que se entienda: la transición se salta, el DOM cambia de golpe y el usuario ve un salto seco. Es, con diferencia, la causa número uno de “las transiciones de vista no me funcionan”, y merece ser lo primero que aprendas del nivel.

🎯 Al terminar esta lección sabrás
  • Reconocer el síntoma de un nombre duplicado y confirmarlo en un segundo.
  • Enumerar las cuatro situaciones que producen duplicados en código correcto.
  • Usar match-element para eliminar el problema donde se puede.
  • Diseñar un esquema de nombres que no pueda colisionar.

La regla y su modo de fallo

view-transition-name acepta none, un identificador o el valor match-element. Cuando le das un identificador, ese elemento se captura por separado y participa en la transición con ese nombre. La restricción es que el valor tiene que ser único entre todos los elementos renderizados en ese instante.

Si no lo es, ocurre esto exactamente:

  1. La promesa ready se rechaza.
  2. La transición se salta: no hay árbol de pseudo-elementos, no hay animaciones.
  3. El callback se ejecuta igual y el DOM queda correctamente actualizado.
  4. finished se resuelve con normalidad.

El punto 3 es el que hace que el fallo sea tan difícil de identificar: la funcionalidad sigue funcionando. La lista se reordena, el panel cambia de contenido, la navegación ocurre. Lo único que falta es la animación, y la reacción natural es pensar que el CSS está mal.

La confirmación es de un segundo si sabes dónde mirar:

const t = document.startViewTransition(mutacion);
t.ready.catch((err) => console.warn('Transicion saltada:', err));

Si ese aviso aparece con un error que menciona nombres duplicados, ya lo tienes. Merece la pena tenerlo puesto de forma permanente en el envoltorio de desarrollo del proyecto, porque el mismo mensaje delata también las otras causas de aborto.

Las cuatro situaciones que lo producen

Nadie escribe dos veces el mismo nombre a propósito. Los duplicados aparecen por cuatro caminos, y los cuatro son código que parece correcto.

El nombre que no se limpió. Asignas view-transition-name a una miniatura antes de la transición para que se convierta en la imagen de detalle, y no lo quitas al terminar. Cuando el usuario vuelve atrás, hay dos elementos con ese nombre: la miniatura vieja y la nueva. Es el caso más común y por eso la limpieza va siempre en finished, con finally para que ocurra también si la transición se interrumpe.

Los dos estados coexistiendo. Durante el callback, el DOM puede tener momentáneamente el elemento viejo y el nuevo a la vez —un framework que inserta antes de borrar, una lista que añade el nuevo elemento y luego elimina el antiguo—. Como la captura del estado nuevo se hace al terminar el callback, si en ese instante los dos siguen ahí, hay duplicado.

El componente instanciado dos veces. Un componente que declara view-transition-name: cabecera en su CSS funciona perfectamente hasta el día en que alguien pone dos en la misma página. El nombre está en una hoja de estilos, no en el marcado, y por eso el duplicado no se ve leyendo el HTML.

El elemento oculto que sigue renderizado. opacity: 0, visibility: hidden y un elemento fuera de pantalla siguen renderizados y por tanto siguen contando para la unicidad. Solo display: none y las tres condiciones que ya vimos —caja fragmentada, contenido saltado, elemento no renderizado— dejan al elemento fuera del recuento. Un carrusel que mantiene todas las diapositivas en el DOM con opacidad cero es una fábrica de duplicados.

La unicidad es por instante, no por documento, y eso abre una puerta

Merece la pena leer la regla con precisión quirúrgica, porque la lectura literal es más permisiva de lo que la gente asume y eso permite un patrón muy útil. La restricción es que dos elementos renderizados no compartan nombre en el momento de cada captura. Hay dos capturas: la del estado antiguo y la del estado nuevo. Y son momentos distintos. Eso significa que el mismo nombre puede pertenecer a un elemento en el estado antiguo y a otro completamente distinto en el nuevo, y eso no solo es legal: es exactamente el mecanismo del morphing. Cuando una miniatura de la rejilla se convierte en la imagen grande del detalle, no es el mismo elemento del DOM: son dos elementos que comparten nombre en momentos distintos, y el navegador los empareja precisamente por eso. La consecuencia de diseño es liberadora: el nombre no identifica un elemento, identifica un rol visual. “La foto protagonista”, “la cabecera”, “el panel principal”. Quien ocupe ese rol en cada momento lleva el nombre. Y de ahí sale la técnica que resuelve el caso del componente duplicado sin renombrar nada: en lugar de dar nombres únicos a todas las instancias, da el nombre solo a la instancia que participa en esta transición concreta y quítaselo a las demás. Una lista de veinte tarjetas donde solo una se expande no necesita veinte nombres: necesita uno, puesto en la que se expande, justo antes de la transición.

match-element, la salida cuando hay muchos

Hay un caso donde la unicidad es genuinamente incómoda: una lista larga cuyos elementos se reordenan y deben moverse a sus nuevas posiciones. Todos participan, todos necesitan nombre, y ponerles nombres únicos a mano significa generarlos desde el índice y mantenerlos sincronizados con el orden.

Para eso existe match-element:

.lista > li {
  view-transition-name: match-element;
}

Con ese valor, el navegador genera internamente un nombre único por identidad de elemento. No hay colisión posible y no hay que escribir ni un índice. El nombre generado no es legible desde el DOM: no lo puedes leer ni usar en un selector, y eso es a la vez su virtud y su límite.

Está en los tres motores: Chromium desde la 137, Firefox desde la 144 y Safari desde la 18.4.

Tiene dos limitaciones que hay que conocer antes de apoyarse en él:

Solo sirve en transiciones de mismo documento. La identidad de un elemento es local a su documento, así que dos documentos distintos nunca generarán el mismo nombre para “el mismo” elemento. En transiciones entre documentos, match-element no puede emparejar nada.

No puedes estilizarlo individualmente. Como el nombre es interno, no hay forma de escribir ::view-transition-group(...) para uno concreto. Para aplicarle estilos hay que usar el comodín o, mejor, view-transition-class, que es la lección que le corresponde.

Un esquema de nombres que no colisiona

Con todo lo anterior, la política que evita el problema de raíz en un proyecto real tiene tres reglas.

Primera: los nombres describen roles, no elementos. vt-hero, vt-panel, vt-cabecera. Un prefijo común hace que sean fáciles de encontrar con una búsqueda de texto, que es la forma en que se detectan los duplicados cuando el proyecto crece. El valor es un identificador de CSS, no un <dashed-ident>: no lleva los dos guiones iniciales de una custom property.

Segunda: los nombres que se asignan dinámicamente se limpian siempre en finished, con finally. Sin excepciones, aunque “esta transición no puede fallar”.

Tercera: ningún componente reutilizable declara un nombre fijo en su CSS. Si un componente puede aparecer dos veces en una página, su nombre tiene que venir de fuera: una custom property, un atributo, o la asignación desde JavaScript en el momento de la transición.

/* En el componente: el nombre viene de fuera y por defecto no hay */
.tarjeta {
  view-transition-name: var(--vt-nombre, none);
}
<article class="tarjeta" style="--vt-nombre: vt-producto-42">...</article>

Ese patrón resuelve el caso del componente duplicado sin tocar el CSS del componente, y deja explícito en el marcado qué instancia participa en la transición. Fíjate en el none por defecto: si nadie asigna la variable, el elemento no se captura por separado, que es el comportamiento seguro.

Un último aviso sobre nombres reservados. El navegador declara view-transition-name: root sobre el elemento raíz en su hoja de estilos, así que usar root para otra cosa produce un duplicado garantizado. Y none no es un nombre, es la ausencia de nombre: no se puede usar como identificador.