Tipos de transición y :active-view-transition-type
Declarar de qué clase es cada transición con types, seleccionar por ella en CSS, y modificar el conjunto en marcha desde el objeto ViewTransition.
Una misma interfaz tiene transiciones que significan cosas distintas: avanzar no es lo mismo que retroceder, abrir un detalle no es lo mismo que ordenar una lista. Hasta que existieron los tipos, distinguirlas obligaba a ensuciar el DOM con clases temporales que había que poner antes y quitar después, con todo lo que eso implica cuando algo falla a la mitad. Los tipos hacen lo mismo de forma declarativa, y la pseudo-clase que los consulta está en los tres motores.
- Declarar tipos al iniciar una transición y seleccionar por ellos en CSS.
- Distinguir
:active-view-transitionde:active-view-transition-type(). - Modificar el conjunto de tipos mientras la transición está en marcha.
- Sustituir una capa de clases temporales por tipos en un caso real.
Declarar y seleccionar
startViewTransition acepta, además de una función, un objeto con dos campos: update con la mutación y types con un array de identificadores.
document.startViewTransition({
update: () => aplicarNavegacion(url),
types: ['navegacion', 'adelante'],
});
En CSS, la pseudo-clase :active-view-transition-type() casa con el elemento raíz cuando la transición activa incluye ese tipo:
html:active-view-transition-type(adelante) {
&::view-transition-old(main) { animation: salir-izquierda 220ms ease-in both; }
&::view-transition-new(main) { animation: entrar-derecha 260ms 100ms ease-out both; }
}
html:active-view-transition-type(atras) {
&::view-transition-old(main) { animation: salir-izquierda 220ms ease-in reverse both; }
&::view-transition-new(main) { animation: entrar-derecha 260ms 100ms ease-out reverse both; }
}
La pseudo-clase acepta varios tipos separados por comas, con semántica de “alguno de estos”:
html:active-view-transition-type(adelante, atras) {
&::view-transition-group(main) { animation-duration: 300ms; }
}
Y existe la versión sin argumento, :active-view-transition, que casa siempre que haya una transición activa, sea del tipo que sea. Sirve para lo que debe aplicarse durante cualquier transición: desactivar interacciones, poner un fondo, ocultar un elemento que estorba.
html:active-view-transition .tooltip {
visibility: hidden;
}
El soporte es completo en los tres motores. :active-view-transition está en Chromium desde la 125, Safari desde la 18 y Firefox desde la 144. La versión con tipos está en Chromium desde la 125, Safari desde la 18.2 y Firefox desde la 147, igual que la propiedad types del objeto ViewTransition.
Qué sustituye
El valor de los tipos se ve mejor comparando con la alternativa. Sin ellos, distinguir el sentido de una navegación exige tres cosas: poner un atributo o una clase en el elemento raíz antes de llamar, escribir selectores que dependan de él, y quitarlo en finished con finally para que no se quede pegado si la transición se interrumpe.
Con tipos, las tres desaparecen. El tipo vive en el objeto de la transición, no en el DOM, y se limpia solo cuando la transición termina, incluso si se aborta. No hay estado residual posible, que es precisamente la clase de bug que producen las clases temporales: una transición interrumpida deja el atributo puesto, la siguiente transición hereda el sentido equivocado, y el usuario ve la animación al revés sin ninguna explicación.
Hay un segundo beneficio menos obvio: los tipos no afectan a la especificidad de tus selectores de componente. Una clase en el raíz obliga a escribir .sentido-atras .tarjeta .titulo, que es un selector más específico y que se cuela en la cascada de tus componentes. :active-view-transition-type() se aplica al raíz y desde ahí solo a los pseudo-elementos de transición, que viven en su propio espacio.
Modificar los tipos en marcha
ViewTransition.types es un conjunto mutable. Se puede leer, añadir y quitar mientras la transición está viva, y el CSS reacciona al instante.
const t = document.startViewTransition({
update: mutar,
types: ['navegacion'],
});
t.ready.then(
() => {
if (document.querySelector('.hero')) t.types.add('con-hero');
},
() => {}
);
Ese patrón —decidir un tipo cuando ya sabes cómo ha quedado el DOM nuevo— resuelve un problema real: a veces no puedes saber de antemano qué clase de transición es hasta que has cargado el contenido. Antes de los tipos, eso obligaba a esperar y luego mutar clases en el raíz a mitad de animación.
El conjunto se comporta como un Set de JavaScript: tiene add, delete, has y clear. Y se vacía automáticamente al terminar la transición, así que no hay que limpiarlo.
La tentación al descubrir los tipos es usarlos para nombrar destinos: a-detalle, a-listado, a-ajustes. Es la decisión que envejece peor y conviene entender por qué antes de tomarla. Un tipo que nombra un destino concreto crea un acoplamiento entre el CSS de transiciones y el mapa de rutas de la aplicación: cada pantalla nueva obliga a añadir un tipo y un bloque de CSS, y el fichero crece linealmente con el producto hasta que nadie sabe qué reglas siguen vivas. Los tipos que envejecen bien nombran la naturaleza del cambio, no su destino, y hay muy pocos: un eje de dirección —adelante y atras—, un eje de profundidad —entrar y salir—, y quizá un eje de intensidad para los cambios que merecen más presencia. Con seis tipos bien elegidos se cubre una aplicación entera, porque cualquier navegación concreta es una combinación de ellos, y los tipos se pueden pasar varios a la vez precisamente para eso. La prueba de que has diseñado bien el vocabulario es que añadir una pantalla nueva no requiere añadir ningún tipo. Si tienes que inventar un tipo cada vez, lo que estás construyendo no es un sistema de movimiento sino una lista de casos particulares disfrazada, y dentro de un año será más barato borrarla entera que entenderla. Este análisis, además, es exactamente el mismo que se aplica a los tokens de duración y de easing de un sistema de diseño: el movimiento se documenta por lo que significa, nunca por dónde ocurre.
Un ejemplo completo
Juntando el vocabulario mínimo con el envoltorio del nivel anterior, así queda una capa de navegación entera:
const RUTAS = ['/', '/blog', '/proyectos', '/sobre-mi'];
function tiposDe(desde, hasta) {
const i = RUTAS.indexOf(desde);
const j = RUTAS.indexOf(hasta);
if (i === -1 || j === -1) return ['salto'];
return [j > i ? 'adelante' : 'atras'];
}
export function navegar(desde, hasta, mutacion) {
if (!document.startViewTransition) {
mutacion();
return;
}
const t = document.startViewTransition({
update: mutacion,
types: tiposDe(desde, hasta),
});
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
t.skipTransition();
}
t.ready.catch(() => {});
return t;
}
/* Comun a cualquier navegacion */
html:active-view-transition {
&::view-transition-group(main) {
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
}
/* Deslizamiento segun el sentido */
html:active-view-transition-type(adelante, atras) {
&::view-transition-old(main) { animation: deslizar-fuera 200ms ease-in both; }
&::view-transition-new(main) { animation: deslizar-dentro 240ms 90ms ease-out both; }
}
html:active-view-transition-type(atras) {
&::view-transition-old(main),
&::view-transition-new(main) { animation-direction: reverse; }
}
/* Un salto entre secciones sin relacion: solo fundido */
html:active-view-transition-type(salto) {
&::view-transition-old(main),
&::view-transition-new(main) { animation-name: none; }
}
@keyframes deslizar-fuera { to { transform: translateX(-3rem); opacity: 0; } }
@keyframes deslizar-dentro { from { transform: translateX(3rem); opacity: 0; } }
Tres tipos, cuatro bloques de CSS y una función de veinte líneas cubren la navegación completa de un sitio. Y lo que hace que esto escale no es ninguna de las piezas por separado: es que el CSS depende del tipo de cambio y no de qué páginas concretas están implicadas, así que añadir una ruta nueva a la constante RUTAS es todo el trabajo que hay que hacer.