isPending: el indicador sutil que no desmonta
El signal pending de useTransition como tercer estado de la interfaz: ni vacío ni resuelto, sino refrescándose sobre contenido que sigue presente. Patrones de indicador sutil que anotan en lugar de reemplazar, atenuar con opacidad, marcar aria-busy, una barra de progreso fina, deshabilitar el envío, sin desmontar nunca el contenido actual. Accesibilidad de la espera, comunicar que algo llega sin robar el foco ni romper la navegación por teclado, y cómo fundir varias transiciones en un único indicador coherente cuando conviven varias fuentes de pendiente.
pending: el estado que Suspense no te daba
useTransition devuelve el par [pending, start]. pending() es un signal booleano que vale true mientras la transición está en vuelo y vuelve a false cuando se compromete. Su valor no lo lees para decidir si pintar contenido —el contenido no se fue a ninguna parte—, sino para decorar la vista existente con una marca de que se está actualizando.
import { createSignal, Suspense, useTransition } from "solid-js";function Panel() { const [pending, start] = useTransition(); const [pestana, setPestana] = createSignal("resumen"); return ( <section aria-busy={pending()} classList={{ atenuado: pending() }}> <Tabs onChange={(t) => start(() => setPestana(t))} /> <Suspense fallback={<Skeleton />}> <Contenido tab={pestana()} /> </Suspense> </section> );}El <Suspense> interno solo actuará en frío, la primera vez. A partir de ahí, cada cambio de pestaña dentro de start mantiene el contenido de la pestaña anterior y enciende pending(). El CSS hace el resto de forma no intrusiva:
.atenuado { opacity: 0.6; transition: opacity 0.2s ease; pointer-events: none; /* opcional: evita clics sobre datos que van a cambiar */}Conviene fijarse en un detalle de timing: pending() sube en cuanto llamas a start, sin esperar un solo milisegundo, así que el feedback es inmediato aunque los datos tarden. Esa inmediatez es la mitad de la sensación de rapidez; la otra mitad es que el contenido nunca se va. El usuario ve, al instante, una vista válida marcada como en curso —jamás un hueco.
Indicadores que anotan, no que reemplazan
La familia de indicadores sutiles comparte un principio: ninguno quita el contenido de en medio. Atenuar con opacidad comunica esto que ves es de hace un instante. Una barra de progreso fina en el borde superior comunica hay actividad sin ocupar espacio de layout. Deshabilitar el botón que disparó la transición evita dobles envíos sin bloquear el resto de la página. Cambiar el cursor a progress da un microfeedback inmediato. Todos son reversibles con un solo signal.
function BarraSuperior(props: { activa: boolean }) { return ( <div class="barra-progreso" classList={{ corriendo: props.activa }} role="presentation" /> );}// <BarraSuperior activa={pending()} /> anclada arriba del todoLa diferencia con el fallback de Suspense es cualitativa, no de grado. El fallback dice aquí no hay nada todavía, espera; el indicador de pending dice aquí hay algo válido, y viene algo mejor. Elegir el segundo cuando de verdad tienes contenido previo es lo que separa una interfaz que respeta la atención del usuario de una que la interrumpe a cada clic.
Accesibilidad: comunicar la espera sin romper el foco
El indicador visual no basta: la espera también debe existir para quien no la ve. El atributo aria-busy="true" sobre la región que se actualiza informa a los lectores de pantalla de que su contenido está en transición. Como el contenido no se desmonta, el foco del teclado permanece donde estaba —un mérito silencioso de las transiciones frente al fallback, que al desmontar tira el foco al body y descoloca la navegación.
<section aria-busy={pending()}> {/* el foco del usuario sobrevive porque nada se desmonta */} <Resultados datos={datos()} /> <div aria-live="polite" class="sr-only"> {pending() ? "Actualizando resultados" : `${datos()?.length ?? 0} resultados`} </div></section>Una región aria-live="polite" que cambie al terminar da la noticia sin interrumpir: anuncia cuántos elementos trajo el filtro nuevo justo cuando la transición resuelve. La combinación es potente: aria-busy durante la espera, aria-live al resolver, y el foco intacto todo el tiempo. La accesibilidad deja de ser un parche y se vuelve una consecuencia natural de no destruir la vista.
Fundir varias transiciones en un indicador
En una pantalla real conviven varias fuentes de pendiente: un filtro, una ordenación, quizá una navegación de fondo. Puedes darle a cada una su useTransition y luego combinar sus signals en un único indicador global con una simple disyunción, para que la barra superior refleje cualquier actividad sin duplicar UI.
const [pFiltro, startFiltro] = useTransition();const [pOrden, startOrden] = useTransition();const ocupado = () => pFiltro() || pOrden();// <BarraSuperior activa={ocupado()} /> anuncia cualquiera de las dosLa decisión de si conviene un indicador único o varios locales depende del significado: si dos transiciones afectan a zonas distintas de la pantalla, dos atenuaciones locales comunican mejor qué está cambiando; si solo quieres una señal global de hay algo en curso, la disyunción basta. Pero como pending es un signal ordinario, componer varios es tan trivial como componer cualquier derivación, y esa es una ventaja que las APIs de carga menos reactivas no te dan.