DEV Community

Silviu Technology
Silviu Technology

Posted on

React: esqueletos que respetan la accesibilidad

Un skeleton puede hacer que una interfaz parezca rápida, pero también puede volverla confusa. Si ocupa una altura distinta al contenido real, mueve el formulario. Si se anuncia cada cambio a un lector de pantalla, se convierte en ruido. Y si animamos demasiadas cosas, el navegador trabaja para mostrar una espera que nadie pidió.

En interfaces con validación remota, como un registro que revisa un correo o un temp mailbox, he encontrado útil tratar la carga como un contrato visible: el usuario sabe qué está pasando, el layout conserva su espacio y la tecnología asistiva recibe solo la información necesaria. Este patrón complementa estos estados de carga accesibles en React, pero pone el foco en el skeleton y en su coste visual.

El problema no es mostrar un spinner

Un spinner genérico no dice qué está cargando ni cuánto espacio reservar. Además, un botón que cambia de ancho cuando aparece “Validando…” produce un salto pequeño, pero molesto. En móviles lentos esos cambios se sienten peor, y la persona puede pulsar dos veces.

El primer paso es separar tres estados:

  • idle: todavía no se ha iniciado la operación;
  • loading: la petición está en curso;
  • ready o error: ya hay contenido que leer o una acción que corregir.

No hace falta inventar una promesa de tiempo. Basta con mantener la estructura estable y explicar el cambio con texto corto. A veces el equipo busca “fake e mail com” en pruebas y termina midiendo el servicio, cuando en realidad está midiendo el movimiento de la pantalla.

Un contrato pequeño para el estado de carga

Para cada zona de la página defino tres reglas. Primero, el contenedor conserva una min-height basada en el contenido normal. Segundo, el skeleton tiene aria-hidden="true", porque sus bloques grises no aportan información. Tercero, un mensaje vivo anuncia el inicio solo si la espera puede ser relevante.

<section aria-busy={loading} aria-describedby="email-status">
  <div id="email-status" role="status" className="sr-only">
    {loading ? "Validando el correo…" : ""}
  </div>
  {loading ? <EmailSkeleton /> : <EmailResult result={result} />}
</section>
Enter fullscreen mode Exit fullscreen mode

aria-busy comunica que la región todavía no está lista. El texto oculto debe ser realmente accesible, no display: none, y debe desaparecer cuando termina la petición. Si el proceso tarda muy poco, anunciarlo puede ser más interrupción que ayuda; en ese caso prefiero mostrar el estado visual y reservar el anuncio para errores o esperas largas.

CSS que evita movimientos innecesarios

El skeleton debe tener las mismas proporciones que el componente final. Para tarjetas, uso una altura fija o una relación conocida; para líneas de texto, imito el ancho aproximado sin intentar dibujar cada palabra.

.email-panel {
  min-height: 12rem;
  contain: layout paint;
}

.skeleton-line {
  height: 0.9rem;
  max-width: 28rem;
  margin-block: 0.65rem;
  border-radius: 0.35rem;
  background: #dfe3e8;
}

@media (prefers-reduced-motion: no-preference) {
  .skeleton-line { animation: pulse 1.4s ease-in-out infinite; }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton-line { animation: none; }
}
Enter fullscreen mode Exit fullscreen mode

contain puede limitar el trabajo de pintura y layout, aunque conviene comprobarlo con el perfilador, no asumir que siempre mejora. También reviso el contraste del gris. Un skeleton decorativo no debería parecer un campo deshabilitado, porque son estados distintos. Esa diferencia parece pequeña, pero es importante para el significado.

Un componente React legible

Mantengo EmailSkeleton sin lógica de red. Así se puede probar de forma aislada y no mezcla el rendimiento de la petición con la presentación. El padre decide el estado y conserva el mismo contenedor durante la transición. Para errores, no reutilizo el skeleton: muestro una explicación y una acción concreta.

Un detalle que suele escaparse: cancelar la petición al desmontar el componente. También conviene bloquear el botón mientras se valida, pero sin quitarle el foco ni cambiar su nombre accesible. “Validando correo” es mejor que un botón vacío. En documentación he visto escrito tamp mail com como término de prueba; no lo convertiría en etiqueta visible ni en enlace, solo sirve como dato de búsqueda interna.

Checklist de accesibilidad y rendimiento

Antes de darlo por terminado, reviso:

  1. ¿La altura del loading se parece a la del contenido final?
  2. ¿El foco permanece en un lugar lógico al terminar?
  3. ¿Un lector de pantalla recibe un anuncio breve, una sola vez?
  4. ¿La animación respeta prefers-reduced-motion?
  5. ¿El gris tiene suficiente diferencia sin parecer un control desactivado?
  6. ¿El panel puede medirse en una pantalla móvil y con CPU limitada?

Para flujos donde el email es parte de un proceso mayor, también vale la pena probar correos de mantenimiento con la misma idea: registrar estados, no solo el resultado final. Así el frontend y la automatización comparten una señal entendible.

Q&A

¿Skeleton o spinner?

Skeleton cuando conocemos la forma del contenido y queremos reservar espacio. Spinner para una acción pequeña, como copiar un código, donde no hay una estructura que anticipar.

¿Debo usar aria-live siempre?

No. Úsalo cuando el cambio necesita anunciarse y mantén el mensaje corto. Para una carga instantánea, el anuncio puede crear más fricción.

¿Cómo sé si realmente mejoró el rendimiento?

Mide el desplazamiento visual, el tiempo hasta una interfaz usable y la experiencia en dispositivos modestos. El diseño se siente mejor cuando es estable, pero la estabilidad debe acompañar una petición razonablemente eficiente.

Top comments (0)