Focus visible y teclado: el 90% de los bugs de accesibilidad están aquí

Ilustración con estilo editorial que muestra una ventana de navegador y navegación por teclado: un botón “Saltar al contenido principal”, tarjetas y un botón “Ver detalle” con contornos de foco resaltados. Alrededor aparecen iconos de Tab, Skip links, Cards clicables y una alerta de “trampas típicas”, junto al título “Focus visible y teclado: el 90% de los bugs de accesibilidad están aquí” y la URL martagonzalez.dev.

Si tu interfaz “se ve bien” pero al navegar con Tab se vuelve un laberinto, no es mala suerte: es el patrón más repetido de bugs de accesibilidad. Y lo peor es que suele pasar incluso en equipos con buen nivel técnico, porque el teclado no se “prueba” con la misma seriedad que el responsive o el rendimiento.

En este artículo vamos a atacar el núcleo: focus visible, orden de tab, skip links, cards clicables y las trampas típicas (modales, menús, overlays, estados, formularios). Te lo cuento de tú a tú, con tono profesional y con ejemplos de diseño e interacción que puedes aplicar hoy.

Importante: muchas auditorías automáticas te dirán “casi ok”. Pero el teclado se valida con manos humanas. Y ahí aparecen los fantasmas.

Por qué el foco es el epicentro (y por qué te rompe la UX si falla)

Cuando navegas con ratón, tu “cursor” es obvio. Con teclado, el cursor es el foco. Si no ves dónde estás, la interfaz deja de ser una interfaz: es una adivinanza.

Aquí es donde entra la comparación clave: tiempo de decisión vs. carga cognitiva.

  • Tiempo de decisión: cuánto tardas en decidir “¿qué hago ahora?”.
  • Carga cognitiva: cuánta energía mental gastas para entender “¿dónde estoy, qué puedo hacer, qué va a pasar?”.

Un focus visible claro reduce ambos:

  • Te ubica en milisegundos (menos tiempo de decisión).
  • Evita que tengas que “reconstruir” el contexto mentalmente (menos carga cognitiva).

Cuando el foco no se ve o el orden de tabulación es caótico, obligas a la persona a:

  1. buscar (¿dónde está el foco?),
  2. inferir (¿qué elemento es este?),
  3. corregir (me pasé, vuelvo con Shift+Tab),
  4. dudar (¿si pulso Enter… rompo algo?).

Y eso en UI complejas (dashboards, filtros, cards, menús) es un multiplicador de frustración.

:focus vs :focus-visible (y cómo dejar de pelearte con los estilos)

Qué es exactamente :focus-visible

  • :focus se activa cuando un elemento recibe foco, venga de teclado, mouse o script.
  • :focus-visible intenta mostrarse solo cuando tiene sentido para la persona usuaria, típicamente cuando navega con teclado.

La consecuencia práctica: puedes diseñar un anillo de foco potente para teclado sin que “moleste” a quien usa ratón.

Regla de oro de CSS (y el pecado mortal del outline: none)

Si has visto esto en un proyecto, es una red flag:

*:focus {
outline: none;
}

Eso es como apagar el foco. Y sí: mucha gente lo hace “para que no se vea feo”. El resultado es que tu UI se vuelve inaccesible por teclado.

La alternativa moderna y segura:

/* Base: dejamos el outline por defecto como fallback */
:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}/* Teclado: anillo visible y consistente */
:focus-visible {
outline: 3px solid currentColor;
outline-offset: 3px;
}/* Opcional: si usas Tailwind, esto suele mapear a ring utilities */

Consejo pro: usa outline (no box-shadow) como primera opción. outline no afecta layout y suele ser más consistente.

El anillo de foco no es “decoración”, es señalización

Un buen focus visible tiene:

  • Contraste suficiente contra el fondo (y también contra el propio componente).
  • Grosor visible (no “una línea finita” que desaparece).
  • Offset para que no se confunda con el borde del botón.
  • Consistencia: mismo estilo en botones, links, inputs, chips, cards.

El problema de “no se ve en fondos con imagen / gradiente”

Si tu UI tiene hero con imagen, tarjetas con fotos o fondos con ruido visual, el foco necesita doble capa:

:focus-visible {
outline: 3px solid #fff;
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(0,0,0,.6);
}

No es para “embellecer”: es para que el foco sobreviva en fondos complejos.

Orden de tab: si el DOM y el layout no cuentan la misma historia, pierdes

El orden de tabulación sigue el orden del DOM (con matices), no el orden visual de tu grid. Por eso, cuando haces layouts con CSS (grid, flex, reorder), puedes crear un recorrido por teclado completamente surrealista.

Las reglas que casi nunca fallan

  1. No uses tabindex positivo (tabindex="1", 2, 3"...).
    • Es un imán de bugs.
    • Rompe expectativas y se vuelve inmantenible.
  2. Mantén el orden lógico en el DOM igual que el visual (siempre que puedas).
  3. Si necesitas reordenar visualmente, piensa si estás creando dos narrativas: una visual y otra para teclado/lectores.
  4. Evita esconder elementos focusables con display:none o visibility:hidden de forma que cambie el orden inesperadamente (especialmente en menús responsive).

El caso típico: filtros + resultados + sidebar

En un e-commerce o listado con filtros:

  • Lo visual suele ser: filtros a la izquierda, resultados a la derecha.
  • Pero si en móvil pones filtros arriba y luego en desktop los “mueves” visualmente sin cambiar el DOM, el tab puede ir: resultados → header → filtros → footer… un caos.

Solución recomendada: estructura DOM con un orden lógico y usa CSS para adaptarlo sin “teletransportar” secciones críticas.

Las skip links (enlace para “saltar al contenido”) son ese detalle que parece pequeño… hasta que tienes que tabular por:

  • logo,
  • navegación,
  • buscador,
  • botones,
  • banners,
  • cookie bar,
  • etc.

Cómo se implementa bien

En el primer foco de la página (normalmente el primer elemento del body), añade:

<a class="skip-link" href="#main-content">Saltar al contenido principal</a><header>...</header><main id="main-content" tabindex="-1">
...
</main>What is this?

Y el CSS clásico (oculta pero accesible, aparece al enfocar):

.skip-link {
position: absolute;
left: -999px;
top: 1rem;
padding: .75rem 1rem;
background: #fff;
border: 2px solid #000;
z-index: 9999;
}.skip-link:focus-visible {
left: 1rem;
}

Por qué tabindex="-1" en main: permite que el salto de foco sea confiable incluso si el navegador no enfoca el destino como esperas.

Si además usas landmarks (<header>, <nav>, <main>, <footer> y/o role="navigation"), ayudas a que la navegación sea más rápida en lectores de pantalla. No es “solo teclado”, es arquitectura.

Cards clicables: el patrón favorito de los bugs silenciosos

Las cards clicables son bonitas. Y peligrosas.

El problema típico:

  • Haces una card con div y onClick.
  • Visualmente parece un link.
  • Con teclado no es interactiva.
  • O lo es “a medias” con tabIndex=0 y role=button, pero luego Enter no navega o Space hace scroll.
  • Y si dentro hay links reales, creas elementos interactivos anidados (error UX + accesibilidad).

Lo más limpio:

<a class="card" href="/detalle">
<h3>Título</h3>
<p>Descripción</p>
</a>What is this?

Ventajas:

  • Tab funciona.
  • Enter funciona.
  • Semántica correcta.

Peeero: solo si la card realmente representa una única acción (ir al detalle).

Si la card tiene botones dentro (guardar, compartir, etc.), no conviertas todo en link. Haz esto:

<article class="card">
<h3>
<a href="/detalle">Título</a>
</h3> <p>Descripción</p> <div class="actions">
<button type="button">Guardar</button>
<button type="button">Compartir</button>
</div>
</article>What is this?

Y si quieres que al clicar en el “fondo” también navegue, hazlo con cuidado: sin romper teclado y sin anidar interactivos. Un truco visual común es usar un pseudo-elemento para ampliar el área clicable del link del título (sin envolverlo todo en un <a>).

Trampa típica — “div role=button” como solución rápida

Sí, se puede hacer accesible… pero es más fácil romperlo que hacerlo bien. Si aun así lo haces:

  • gestiona Enter y Space,
  • añade aria-label si no hay texto,
  • asegura estilos de foco,
  • y recuerda que un “botón que navega” suele ser peor que un link.

Formularios accesibles: foco, errores y mensajes que no se pierden

Ya que la keyword secundaria incluye formularios accesibles, aquí es donde el foco se vuelve crítico.

Error summary + foco al primer error

Cuando envías un formulario con errores:

  1. Muestra un resumen arriba (“Revisa los campos marcados”).
  2. Lleva el foco a ese resumen.
  3. Desde ahí, ofrece links a cada campo con error.

Ejemplo conceptual:

<div class="error-summary" role="alert" tabindex="-1" id="error-summary">
<p><strong>Hay errores en el formulario</strong></p>
<ul>
<li><a href="#email">El email no es válido</a></li>
<li><a href="#password">La contraseña es obligatoria</a></li>
</ul>
</div>What is this?

Y al fallar validación:

document.getElementById("error-summary")?.focus();

aria-describedby para unir campo + ayuda + error

<label for="email">Email</label>
<input id="email" aria-describedby="email-hint email-error" /><p id="email-hint">Usa un correo real. Te enviaremos confirmación.</p>
<p id="email-error" role="status">Formato incorrecto.</p>What is this?

Clave: si el error aparece dinámicamente, asegúrate de que se anuncie y de que el foco no “salte” sin motivo.

Trampas típicas con teclado (las que más veo en UI reales)

Modales sin focus trap (o con focus trap agresivo)

Un modal accesible necesita:

  • foco inicial dentro (idealmente en el título o el primer control),
  • focus trap para que Tab no se vaya detrás,
  • cierre con Escape,
  • restaurar el foco al elemento que lo abrió.

Solución moderna: inert para el fondo

Cuando el modal está abierto, el resto de la página debería quedar “inactivo”. inert ayuda mucho (según soporte del navegador / polyfill si lo necesitas):

const main = document.querySelector("main");
main?.setAttribute("inert", "");
// al cerrar:
main?.removeAttribute("inert");

Menús desplegables que se cierran al perder foco

Si tu dropdown se cierra cuando “blur” ocurre, puedes crear un infierno:

  • tabulas al siguiente item y se cierra antes de poder interactuar,
  • o se cierra al intentar usar flechas.

Solución: define claramente el patrón (menú tipo navegación vs select vs combobox). Y si no estás segura, apóyate en patrones ARIA bien establecidos (y pruébalo con teclado real).

“Scroll jail”: foco dentro de contenedores con overflow

Cuando metes listas scrollables (overflow:auto) y no gestionas bien el foco:

  • el foco se mueve pero la lista no hace scroll para mostrarlo,
  • parece que “desapareció”.

Solución: al cambiar foco dentro de un contenedor scrollable, asegúrate de que el elemento se vea (element.scrollIntoView({ block: "nearest" }) con moderación).

Checklist de supervivencia: prueba manual que detecta el 90% de fallos

Recorrido mínimo (5 minutos por pantalla)

  1. Tab desde el inicio: ¿aparece focus visible siempre?
  2. ¿El foco se ve en botones, links, inputs, chips, icon buttons?
  3. ¿El orden de tab es lógico y coincide con el recorrido visual?
  4. ¿Hay skip link? ¿Funciona?
  5. ¿Puedes activar todo con Enter y/o Space cuando corresponde?
  6. ¿Hay algún “callejón sin salida” (no puedes salir de un componente)?
  7. En formularios: ¿al error te enteras, y sabes qué campo corregir?

Antipatrones que debes cazar como si fueran pokémon

  • outline: none sin alternativa.
  • tabindex="1" (o cualquier positivo).
  • Cards enteras clicables con div onClick sin semántica.
  • Modales que abren y el foco se queda detrás.
  • Componentes “bonitos” que solo funcionan con mouse.

Enlaces internos recomendados (para hilar la serie)

Para completar el mapa mental y reforzar SEO interno, enlaza desde aquí a:

  • Links accesibles: “haz click aquí” es un crimen → (añade tu URL final)
    Sugerencia de slug: /blog/links-accesibles-haz-click-aqui-es-un-crimen/
  • Componentes UI accesibles (patrones) → (añade tu URL final)
    Sugerencia de slug: /blog/componentes-ui-accesibles-patrones/

(Tip SEO: enlaza también de vuelta desde esos posts hacia este, usando el anchor “focus visible” o “navegación con teclado”.)

FAQs (preguntas frecuentes)

1) ¿focus-visible funciona en todos los navegadores?

En la mayoría de navegadores modernos, sí. Aun así, conviene dejar un fallback con :focus para no quedarte sin estilos si algo falla. La estrategia habitual es: focus como base + focus-visible como mejora.

2) ¿Por qué no debería usar tabindex positivo para “arreglar” el orden?

Porque es pan para hoy y deuda técnica para mañana. En cuanto cambias algo del DOM, el orden se vuelve impredecible y cuesta muchísimo mantenerlo. Lo correcto es arreglar el orden en el DOM y reservar tabindex para casos muy concretos (0 o -1).

3) ¿Cómo hago una card clicable sin romper accesibilidad?

Piensa primero si la card representa una única acción. Si sí, usa un <a> envolviendo el contenido. Si no (porque hay botones dentro), usa link principal (título) + acciones separadas. Evita anidar interactivos y evita div clicables como atajo.


Accesibilidad es bajar el volumen mental

Un buen focus visible no es un “detalle de a11y”. Es un contrato de claridad: “estás aquí, puedes hacer esto, y esto es lo que pasará”. Cuando ese contrato se rompe, sube el tiempo de decisión y explota la carga cognitiva. Y eso no afecta solo a personas con discapacidad: afecta a cualquiera que navegue rápido, con prisa, con fatiga, con una mano ocupada, o simplemente con preferencia por teclado.

Si quieres un criterio simple para priorizar: si una pantalla funciona perfecta con teclado, suele funcionar mejor para todo el mundo. Y además, te obliga a diseñar interacciones más honestas: menos trucos visuales, más intención semántica, más estructura.

La próxima vez que alguien diga “esto es solo accesibilidad”, prueba a responder:
“No: esto es usabilidad bajo presión.” Y el teclado es la prueba de fuego.

Formularios accesibles: labels, errores y validación sin frustrar a nadie

Ilustración de un formulario web con mensajes de error accesibles: aviso “El email no es válido”, resumen de errores y pistas como “aria-describedby” y “Focus al primer error”, con el título “Formularios accesibles: labels, errores y validación sin frustrar a nadie”.

Los formularios accesibles en HTML son una de las bases más importantes de cualquier interfaz usable. Un formulario puede parecer sencillo, pero si los labels no están bien asociados, los errores no se entienden o la validación llega demasiado tarde, la experiencia de usuario se rompe rápidamente.

Crear formularios accesibles en HTML no consiste solo en añadir atributos ARIA o cumplir una checklist técnica. También implica pensar en cómo una persona entiende cada campo, cómo recibe ayuda, cómo corrige un error y qué ocurre cuando navega con teclado o lector de pantalla.

La clave está en combinar buena semántica, instrucciones claras y validación comprensible. Un label bien asociado, un mensaje de error útil y una ayuda conectada con aria-describedby pueden marcar una gran diferencia entre un formulario frustrante y un flujo realmente accesible.

En este artículo veremos cómo diseñar formularios accesibles en HTML usando labels claros, mensajes de ayuda, errores bien comunicados y validaciones que acompañen a la persona usuaria sin bloquearla ni hacerle repetir pasos innecesarios.

La base no negociable: el “nombre accesible” y los labels bien hechos

Un formulario accesible empieza con una idea simple: cada control debe tener un nombre accesible claro. Ese nombre es lo que anuncia un lector de pantalla, lo que entiende el autocompletado, y lo que ayuda a cualquier persona (incluida la que no usa tecnologías de apoyo) a completar el formulario más rápido.

Label visible y asociado: lo más robusto

La opción más estable sigue siendo la de siempre:

<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" />What is this?
  • Visible: reduce dudas (“¿qué se supone que va aquí?”).
  • Asociado con for/id: funciona con teclado, lectores de pantalla y click/tap.
  • Compatible con traducciones y QA (se testea fácil).

Cuándo agrupar: fieldset + legend

Si tienes opciones relacionadas (radio buttons, checkboxes en grupo), no uses un label “falso” arriba y ya. Agrupa:

<fieldset>
<legend>Método de contacto preferido</legend> <div>
<input type="radio" id="contact-email" name="contact" value="email" />
<label for="contact-email">Email</label>
</div> <div>
<input type="radio" id="contact-phone" name="contact" value="phone" />
<label for="contact-phone">Teléfono</label>
</div>
</fieldset>What is this?

Esto mejora comprensión y reduce carga cognitiva: el usuario no tiene que deducir qué relación tienen esos controles.

No, el placeholder no es un label (y suele ser una trampa)

El placeholder:

  • desaparece al escribir (adiós referencia),
  • suele tener bajo contraste,
  • se confunde con “texto ya rellenado”,
  • y en móvil es aún peor.

Si quieres dar ejemplo o formato, usa texto de ayuda persistente.

Ayudas y ejemplos sin ensuciar: texto de hint

<label for="dni">DNI</label>
<p id="dni-hint">Formato: 12345678X</p>
<input id="dni" name="dni" aria-describedby="dni-hint" />What is this?

Aquí ya asoma el protagonista de este artículo: aria-describedby.

Errores que ayudan, no que castigan

Un error accesible no es “ponerlo en rojo”. Un error útil responde a tres preguntas:

  1. Qué pasó (qué está mal)
  2. Por qué importa (si aplica)
  3. Cómo lo arreglo (acción concreta)

Mensajes de error con microcopy que baja la frustración

Ejemplos malos:

  • “Valor inválido”
  • “Error”
  • “Campo incorrecto”

Ejemplos buenos:

  • “Introduce un email válido, por ejemplo: nombre@dominio.com
  • “La contraseña debe tener al menos 10 caracteres y 1 número”
  • “Este campo es obligatorio”

Tip UX importante: evita el tono regañón. Un formulario no es una relación tóxica.

Dónde mostrar errores: inline + resumen (cuando el formulario es largo)

Para formularios cortos, suele bastar con error inline cerca del campo. Para formularios largos o con envío final, un resumen de errores arriba ahorra tiempo y reduce abandono.

  • Inline: repara el error en contexto.
  • Resumen: evita la “búsqueda del tesoro” cuando hay varios fallos.

Señales de error sin depender solo del color

Asegúrate de combinar:

  • color + icono + texto,
  • borde/outline suficiente,
  • mensaje explícito,
  • y, si puedes, un patrón consistente (siempre mismo lugar y estilo).

Esto impacta directamente en el equilibrio tiempo de decisión vs. carga cognitiva: si el usuario tiene que interpretar señales ambiguas, decide más lento y se cansa antes.

ARIA aplicada a formularios: aria-describedby, aria-invalid, mensajes y anuncios

ARIA no “arregla” un formulario mal construido, pero sí puede hacerlo entendible cuando la UI es dinámica o compleja.

aria-describedby: une el campo con su ayuda y su error

La idea: el input “apunta” a uno o varios elementos que amplían su explicación.

Caso 1: hint permanente

<label for="password">Contraseña</label>
<p id="password-hint">Mínimo 10 caracteres, con 1 número.</p>
<input
id="password"
name="password"
type="password"
aria-describedby="password-hint"
/>What is this?

Caso 2: hint + error (cuando aparece)

<label for="password">Contraseña</label>
<p id="password-hint">Mínimo 10 caracteres, con 1 número.</p><p id="password-error" hidden>
La contraseña debe tener al menos 10 caracteres e incluir 1 número.
</p><input
id="password"
name="password"
type="password"
aria-describedby="password-hint password-error"
aria-invalid="false"
/>What is this?

Cuando validas y hay error:

  • quitas hidden,
  • pones aria-invalid="true".

Detalle clave: si el error se actualiza dinámicamente, asegúrate de que se anuncie (ahora vamos con eso).

aria-invalid y aria-errormessage

  • aria-invalid="true" marca el control como inválido.
  • aria-errormessage="id-del-error" puede usarse para apuntar al mensaje de error.

Ejemplo:

<p id="email-error" hidden>Introduce un email válido.</p><input
id="email"
type="email"
aria-invalid="true"
aria-errormessage="email-error"
aria-describedby="email-error"
/>What is this?

En la práctica, aria-describedby sigue siendo el más compatible para “leer” el error en contexto. aria-errormessage puede complementar, pero no lo uses como “única vía”.

Cómo anunciar errores en tiempo real sin saturar

Si actualizas errores al vuelo, puedes usar un contenedor con aria-live:

<div id="form-status" aria-live="polite"></div>What is this?
  • polite: anuncia cuando pueda (mejor para no interrumpir).
  • assertive: interrumpe (úsalo solo para cosas críticas).

Regla de oro: no conviertas el formulario en una máquina de notificaciones. Si anuncias cada letra, creas ruido y subes la carga cognitiva.

Focus al primer error: menos vueltas, más claridad

“Focus al primer error” es un patrón muy citado porque reduce el tiempo de resolución: el usuario envía, hay errores, y en vez de dejarlo arriba del todo preguntándose qué pasó, lo llevas al primer campo inválido.

Cuándo es buena idea

  • Formulario con botón “Enviar” al final.
  • Errores que solo se detectan al submit (servidor / reglas complejas).
  • Formularios largos (checkout, alta, onboarding).

Cómo hacerlo sin romper la UX

Aquí tienes un patrón que suele funcionar mejor que “foco directo al input sin contexto”:

  1. Muestras resumen de errores arriba.
  2. Mueves foco al resumen (para anunciar lo que pasó).
  3. El resumen contiene enlaces a cada campo con error.
  4. Opcional: el primer enlace apunta al primer campo inválido.

Ejemplo de resumen:

<div
id="error-summary"
role="alert"
tabindex="-1"
aria-labelledby="error-summary-title"
hidden
>
<h2 id="error-summary-title">Revisa estos campos</h2>
<ul>
<li><a href="#email">El email no es válido</a></li>
<li><a href="#password">La contraseña no cumple los requisitos</a></li>
</ul>
</div>What is this?
  • role="alert" ayuda a anunciar el bloque.
  • tabindex="-1" permite llevar el foco ahí con JS.
  • Los enlaces con href="#id" facilitan salto con teclado y lector.

Luego, en JS (concepto, no framework específico):

  • si hay errores → mostrar resumen → focus() al resumen.

Ojo con “robar foco” mientras el usuario escribe

No cambies el foco al primer error en medio de la escritura. Eso desespera. Una estrategia menos invasiva:

  • valida on blur (cuando sale del campo),
  • o después de un pequeño delay,
  • y deja el “focus al primer error” para el submit.

Validación sin frustración: interacción, prevención y accesibilidad real

Validar no es solo “bloquear”. Validar bien es prevenir errores.

Tiempo de decisión vs carga cognitiva: por qué tu formulario se siente “pesado”

  • Tiempo de decisión: cuánto tarda alguien en elegir qué hacer (qué opción, qué formato, qué respuesta).
  • Carga cognitiva: cuánta energía mental gasta entendiendo y recordando cosas.

Formularios que suben ambos:

  • demasiadas opciones sin jerarquía,
  • campos con requisitos ocultos,
  • formatos raros (teléfono, fechas) sin ayuda,
  • errores genéricos,
  • pasos que no explican “por qué te pido esto”.

Formularios que los reducen:

  • defaults inteligentes (sin ser tramposos),
  • progresive disclosure (mostrar solo lo necesario),
  • ejemplos claros (hint + aria-describedby),
  • validación amable (no punitiva),
  • feedback inmediato pero no ruidoso.

Prevención: input types, autocomplete, inputmode y máscaras con cuidado

  • type="email", type="tel", type="number" (con criterio), type="date" (ojo compatibilidad).
  • autocomplete="email", autocomplete="name", autocomplete="postal-code"… ayuda muchísimo.
  • inputmode="numeric" para móviles cuando quieres números (mejor que type="number" en algunos casos).
  • Máscaras: úsalas solo si no impiden editar. Si la máscara dificulta corregir, sube frustración.

Obligatorio no es lo mismo que “marcar con *”

Si usas asterisco:

  • acompáñalo de texto (“Campos obligatorios *”),
  • y no dependas solo del símbolo para que se entienda.

En HTML, puedes usar required y, si quieres, comunicarlo en el label:

  • “Email (obligatorio)”.

Ejemplos UI avanzados (con patrones que suelen posicionar bien)

1) Login simple: error inline + anuncio suave

  • Error debajo del campo.
  • aria-describedby enlaza a error cuando aparece.
  • aria-live="polite" para estados generales (por ejemplo, “credenciales incorrectas”).

2) Checkout: resumen arriba + foco al resumen + enlaces a campos

  • Reduce búsqueda visual.
  • Mejora navegación con teclado.
  • Disminuye el “¿qué demonios pasó?” tras pulsar pagar.

3) Newsletter: validación al salir del campo (blur), no en cada tecla

  • Evita spam de errores (“falta @” cuando aún estás escribiendo).
  • Más humano, menos robot.

4) Formulario en modal (si lo usas): no olvides el contexto

Si el formulario está dentro de un modal:

  • el foco debe quedar atrapado en el modal,
  • cerrar con Escape,
  • y los errores deben anunciarse dentro (aquí enlaza con tu post de Componentes UI accesibles, porque los modales son un mundo).

Checklist técnico para auditar “formularios accesibles” (nivel pro)

  • Cada input tiene label asociado (for/id) o nombre accesible equivalente.
  • Los grupos de radios/checkboxes usan fieldset + legend.
  • Los placeholders no sustituyen labels.
  • Los hints y errores están conectados con aria-describedby.
  • Cuando hay error: aria-invalid="true" (y mensaje visible).
  • Los errores no dependen solo del color (texto + patrón visual).
  • Existe un patrón claro de validación (on blur / on submit) sin bombardear.
  • Si hay varios errores: hay resumen arriba y es alcanzable por teclado.
  • Implementación de focus al primer error (preferiblemente foco al resumen primero).
  • Estados dinámicos anunciados con aria-live cuando corresponde (sin exceso).
  • Contraste suficiente en texto, borde y estados (focus/error/disabled).
  • Soporte móvil: autocomplete, inputmode, tamaños de tap cómodos.

Preguntas frecuentes (FAQs)

1) ¿aria-describedby debe apuntar al error, al hint o a ambos?

A ambos, si ambos aportan valor. Lo típico es hint siempre + error solo cuando aparece. Mantén los IDs estables y actualiza visibilidad/estado, para que el control siempre “arrastre” la información correcta.

2) ¿Es obligatorio mover el foco al primer error al enviar?

No es obligatorio, pero suele ser recomendable en formularios largos. En muchos casos, el patrón más amable es: foco al resumen de errores (para contexto) y desde ahí enlaces al primer error. Mover foco directamente al input puede desorientar si el usuario no entiende “qué pasó”.

3) ¿Valido en tiempo real o al submit?

Depende, pero una regla práctica es:

  • en tiempo real solo para ayudas útiles (fortaleza de contraseña, formato orientativo),
  • on blur para errores simples,
  • on submit para reglas complejas o validación de servidor.
    La prioridad es no interrumpir y no saturar: valida para ayudar, no para castigar.

La accesibilidad en formularios es empatía aplicada (y SEO del bueno)

Un formulario accesible no es “cumplir una checklist”. Es diseñar una conversación donde la otra persona se siente guiada, no examinada. Cuando etiquetas bien, reduces dudas. Cuando los errores explican y se anuncian, reduces frustración. Cuando gestionas el foco con intención, reduces vueltas. Y cuando equilibras tiempo de decisión vs carga cognitiva, reduces abandono.

La parte bonita: esto no solo beneficia a quien usa lector de pantalla. Beneficia a todo el mundo. Y además, en términos de posicionamiento, un artículo que baja a tierra patrones como aria-describedby, resumen de errores y focus management suele destacar porque responde exactamente a lo que la gente busca cuando escribe “formularios accesibles errores aria-describedby focus al primer error ejemplos UI”.

Toasts/notificaciones accesibles: aria-live sin volver loco al usuario

Ilustración de una aplicación web mostrando tres notificaciones tipo toast anunciadas mediante aria-live: un mensaje de éxito, uno informativo y una advertencia, representando cómo comunicar cambios de estado sin interrumpir la interacción del usuario.

Los toasts (o “snackbars”, avisos flotantes, notificaciones breves) son ese tipo de UI que parece inocente… hasta que te das cuenta de que puede convertirse en una máquina de interrumpir personas. Si los haces mal, suben la carga cognitiva, rompen el foco, saturan lectores de pantalla y, en el peor caso, hacen que el usuario pierda confianza: “¿Qué ha pasado? ¿Se guardó? ¿Falló? ¿Dónde miro ahora?”

La gracia está en el equilibrio: anunciar lo importante sin convertir cada micro-evento del sistema en un megáfono. Aquí entra aria-live, pero no como “ponlo y listo”, sino como una herramienta que hay que diseñar: qué se anuncia, cuándo, con qué prioridad, con qué texto y con qué interacción.

En este artículo vamos a bajar a tierra decisiones reales: role, aria-live, duración, cola de mensajes, acciones, foco, patrones UI y ejemplos de implementación. Y, sí, vamos a comparar algo clave: tiempo de decisión vs. carga cognitiva (porque los toasts influyen en ambos).

Qué es un toast (y por qué la accesibilidad aquí no es “extra”)

Un toast es una notificación breve que suele aparecer en una esquina o borde de la pantalla, se mantiene unos segundos y desaparece. Lo usamos para:

  • Confirmaciones: “Guardado”, “Copiado”, “Añadido a favoritos”.
  • Errores: “No se pudo guardar”, “Conexión perdida”.
  • Estados: “Sincronizando…”, “Modo offline”.
  • Acciones rápidas: “Elemento eliminado — Deshacer”.

El problema: no todos los usuarios “ven” un toast. Algunas personas usan lector de pantalla, otras tienen baja visión, otras están en móvil con el pulgar tapando media pantalla, otras van con prisa, cansancio o estrés. Si tu toast solo existe “en lo visual”, estás dejando fuera a una parte de tu audiencia (y, además, estás haciendo UX más frágil para todos).

Aquí entra el objetivo real:

  • UX: que el usuario entienda qué pasó y qué hacer.
  • A11y: que esa información sea perceptible y operable sin depender de la vista ni del timing.

Tiempo de decisión vs. carga cognitiva: el coste oculto de un toast mal hecho

Dos métricas mentales que se disparan con notificaciones mal diseñadas:

Tiempo de decisión

Es el tiempo que tardas en responder mentalmente: “¿Esto requiere algo de mí?”. Si tu toast es ambiguo (“Error”), el usuario necesita más tiempo para decidir.

Carga cognitiva

Es el esfuerzo total para entender y gestionar lo que ocurre. Si tus toasts aparecen cada 2 segundos (o narran cosas irrelevantes), el usuario se fatiga, pierde el hilo y comete más errores.

Un toast accesible y bien diseñado reduce ambas:

  • Texto claro → baja tiempo de decisión.
  • Prioridad correcta + frecuencia controlada → baja carga cognitiva.
  • Acciones coherentes (“Deshacer”) → menos fricción.

aria-live explicado como si lo fueras a usar mañana

Las live regions (regiones en vivo) permiten que un lector de pantalla anuncie cambios dinámicos sin que el usuario tenga que mover el foco.

Conceptos que debes conocer (sí o sí)

aria-live

Define la urgencia del anuncio.

  • polite: el lector de pantalla anuncia cuando puede, sin interrumpir.
  • assertive: interrumpe para anunciar inmediatamente (úsalo con pinzas).

role

Algunos roles ya implican comportamientos típicos:

  • role="status" suele equivaler a anuncio polite.
  • role="alert" suele equivaler a anuncio assertive.

Regla práctica: status para confirmaciones y estados, alert para errores graves o bloqueantes.

aria-atomic="true"

Hace que se anuncie el mensaje completo cuando cambia (en lugar de solo el fragmento modificado). Para toasts, suele ser buena idea.

aria-relevant

Controla qué cambios se anuncian (añadidos, removidos, texto). En toasts, normalmente no necesitas tocarlo si actualizas el texto entero.

Elegir el patrón correcto: ¿toast, banner, diálogo o inline?

Antes de tocar ARIA, pregunta esto:

¿El usuario necesita actuar ahora?

  • Si , quizá no es un toast. Puede ser:
    • Inline error junto al campo (ideal en formularios).
    • Banner persistente arriba (“Hay errores en el formulario”).
    • Diálogo si bloquea la tarea (con moderación).

¿Es solo informativo y no bloquea?

  • Toast funciona bien: “Guardado”, “Copiado”, “Añadido…”.

¿Es un error crítico?

  • Toast puede valer si además ofreces ruta clara (“Reintentar”, “Ver detalles”) o si duplicas el mensaje en un lugar persistente.

Antipatrón clásico: usar toast para errores de validación en campos. Eso sube el tiempo de decisión (“¿qué campo?”) y la carga cognitiva (“tengo que buscar dónde falló”).

Diseñar el mensaje: microcopy que se entiende en 1 segundo

Tu texto tiene que ser “escaneable”, porque un toast es breve y el usuario no está “leyendo”, está haciendo otra cosa.

Fórmula útil

Qué pasó + dónde/qué afecta + qué puedo hacer

Ejemplos mejores:

  • ✅ “Ruta guardada en Favoritos.”
  • ✅ “No se pudo guardar la ruta. Reintenta.”
  • ✅ “Conexión perdida. Estás en modo offline.”
  • ✅ “Foto eliminada. Deshacer.”

Ejemplos peores:

  • ❌ “Error”
  • ❌ “Operación realizada”
  • ❌ “Algo salió mal”

Longitud y tono

  • Evita tecnicismos (“HTTP 500”) salvo en herramientas para usuarios técnicos.
  • Si hay acción, ponla en verbo claro: Reintentar, Deshacer, Ver.

Implementación accesible: estructura y ARIA que no molestan

Aquí está la clave: no todo toast merece ser anunciado.

Prioridad de anuncios (guía práctica)

1) Confirmaciones suaves → role="status" / aria-live="polite"

  • “Guardado”
  • “Copiado”
  • “Añadido”

2) Errores importantes → role="alert" (con moderación)

  • “No se pudo pagar”
  • “Sesión caducada”
  • “Permiso denegado”

3) Ruido → NO se anuncia

  • “Sincronizando… 1%… 2%… 3%…”
  • “Auto-guardado cada 5s”
  • “Actualizando lista…”

Si algo se actualiza continuamente, anúncialo solo al inicio y al final, o muévelo a un estado visible persistente (ej. barra de estado).

Evitar el “lector de pantalla ametralladora”: colas, deduplicación y throttling

Si tu app dispara muchos eventos, necesitas control.

Estrategias reales que funcionan

Cola de mensajes

Muestra y anuncia uno a la vez. Si hay 5, no los sueltes juntos. En cola:

  • Anuncia el actual.
  • Cuando desaparece, pasa al siguiente.

Deduplicación

Si llega el mismo mensaje varias veces (“Guardado”), agrúpalo:

  • “Guardado (x3)” o
  • No repitas si ocurrió hace < 2–3 segundos.

Throttling por tipo

  • Estados “suaves”: máximo 1 cada X segundos.
  • Errores: siempre, pero con copy útil y acción.

Esto reduce carga cognitiva para todos y, especialmente, para usuarios con lector de pantalla.

Duración, pausa y control: el toast no puede ser una bomba de tiempo

Duración recomendada

  • Confirmaciones: 3–5s.
  • Mensajes con acción (Deshacer): 6–10s (o persistente hasta interacción si es crítico).
  • Errores: considera persistencia o que el usuario pueda cerrarlo manualmente.

¿Debe ser “cerrable”?

  • Si es polite y no crítico, no siempre hace falta.
  • Si interrumpe, contiene acción o puede tapar contenido: , botón “Cerrar”.

Accesibilidad del autocierre

Si el usuario necesita actuar (ej. “Deshacer”), no lo cierres demasiado rápido. Y cuidado: un toast que desaparece antes de que el lector de pantalla lo termine de anunciar es frustrante.

Foco: cuándo NO debes moverlo (casi siempre)

Un toast, por defecto, no debería robar foco. Si lo hace:

  • Rompe el flujo de teclado.
  • Aumenta tiempo de decisión (“¿dónde estoy?”).
  • Multiplica carga cognitiva.

Excepciones

  • Si el toast realmente es un “diálogo disfrazado” porque exige decisión inmediata, entonces no es toast: usa un diálogo accesible con foco gestionado (y probablemente un overlay).

Ejemplos de implementación

Ejemplo 1: Región live global (recomendada) + visual toast independiente

La idea: separas la capa “anuncio accesible” de la capa visual. Así controlas mejor qué se anuncia y cuándo, sin depender del DOM del componente visual.

<!-- Live region (puede estar cerca del root) -->
<div id="live-region"
     role="status"
     aria-live="polite"
     aria-atomic="true"
     class="sr-only">
</div>

<!-- Contenedor visual de toasts -->
<div class="toast-stack" aria-label="Notificaciones">
  <!-- Cada toast visual NO necesita aria-live -->
</div>

Cuando ocurre un evento que quieras anunciar, actualizas el texto de #live-region.

Ejemplo 2: Error crítico con role="alert"

<div id="live-error"
     role="alert"
     aria-atomic="true"
     class="sr-only">
</div>

Úsalo para fallos importantes. Y si hay acción, ponla cerca del lugar donde el usuario pueda actuar (no solo en el toast).

Diseño e interacción: patrones de toasts que no rompen la UX

Patrón recomendado: “Acción reversible”

Cuando el usuario elimina algo:

  • Toast: “Elemento eliminado. Deshacer.”
  • Acción real: retrasas el borrado definitivo unos segundos, o guardas un snapshot para revertir.

Esto reduce ansiedad y mejora decisión.

Patrón recomendado: “Error + siguiente paso”

En lugar de “Error al guardar”:

  • “No se pudo guardar. Reintentar.”
  • “No se pudo guardar. Comprueba tu conexión.”
  • “No se pudo guardar. Vuelve a iniciar sesión.”

Menos tiempo de decisión, menos carga cognitiva.

Patrón recomendado: “Estados persistentes para procesos largos”

En vez de 20 toasts:

  • Una zona de estado: “Subiendo 3 archivos…”
  • Y toast solo al final: “Subida completada.”

Testing: cómo saber si tu solución funciona de verdad

Checklist rápido

Con teclado

  • ¿Puedes seguir trabajando sin que el toast te saque del foco?
  • ¿El botón “Cerrar” es alcanzable si existe?
  • ¿La acción “Deshacer” es accesible por teclado?

Con lector de pantalla

  • ¿Se anuncia lo importante y solo lo importante?
  • ¿No se cortan mensajes por updates rápidos?
  • ¿No se repiten confirmaciones sin sentido?

En móvil

  • ¿No tapa botones críticos?
  • ¿No queda debajo de elementos sticky?
  • ¿Se puede cerrar fácilmente?

Detalle importante

Si tu toast aparece en el DOM, se re-renderiza, cambia de orden o se destruye demasiado rápido, el lector de pantalla puede:

  • anunciarlo tarde,
  • no anunciarlo,
  • o anunciarlo incompleto.

Por eso, muchas veces conviene la estrategia de live region global.

Errores típicos (y cómo evitarlos)

1) Convertir todo en assertive

Esto es el “modo sirena”. El usuario acaba saturado, y con lector de pantalla es directamente agresivo.

Solución: polite por defecto, alert solo en lo crítico.

2) Toast para validación de formularios

El usuario escucha “Hay un error”, pero no sabe dónde.

Solución: error inline + resumen arriba (banner) + foco al primer error si envía el formulario.

3) Mensajes ambiguos

“Operación completada” no significa nada.

Solución: explica qué se completó y qué implica.

4) Autocierre demasiado rápido con acción

Si hay “Deshacer”, 2 segundos no sirven.

Solución: más tiempo o persistente hasta interacción.

Checklist final para “toasts accesibles de verdad”

Semántica y ARIA

  • Usa role="status" + aria-live="polite" para confirmaciones.
  • Usa role="alert" solo para errores importantes.
  • Añade aria-atomic="true" si actualizas texto.
  • Considera una live region global para anuncios.

UX

  • Mensaje claro: qué pasó + impacto + siguiente paso.
  • No robes foco.
  • Controla frecuencia (cola/deduplicación).
  • Si hay acción, da tiempo suficiente.

Diseño

  • No tapes CTAs importantes.
  • Respeta responsive y barras sticky.
  • Mantén jerarquía visual: error ≠ confirmación.

Preguntas frecuentes (FAQs)

¿Es mejor role="alert" o aria-live="assertive" para errores?

En la práctica, role="alert" suele ser la opción directa para errores urgentes porque ya está pensado para avisos importantes. Pero lo clave no es el atributo: es cuándo lo usas. Si todo es “urgente”, nada lo es. Reserva alert/assertive para casos donde el usuario realmente necesita enterarse ya (fallo de pago, sesión caducada, bloqueo de acción).

¿Un toast debería ser “cerrable” siempre?

No siempre. Si es una confirmación leve (“Copiado”), puede autocerrarse sin botón. Pero si el toast:

  • tapa contenido,
  • incluye una acción (“Deshacer”),
  • o es un error que el usuario puede querer revisar,
    entonces , añade “Cerrar” (y que sea accesible por teclado).

¿Dónde coloco la live region para que funcione bien?

Idealmente, cerca del root de tu app (layout principal), para que sea estable y no se destruya con cambios de ruta. Lo importante es que:

  • exista siempre,
  • se actualice solo cuando corresponde,
  • y no reciba “spam” de mensajes.

La accesibilidad es “diseño de interrupciones”

Los toasts son, literalmente, una interrupción. Y diseñar interrupciones bien es un acto de respeto: respeto por el foco, por el tiempo del usuario y por su energía mental.

Un toast accesible no es “poner aria-live”. Es decidir con criterio qué merece ser anunciado, escribir un mensaje que se entiende al vuelo y crear un comportamiento que no secuestra la atención. Cuando lo haces bien, bajas el tiempo de decisión (“entiendo qué pasó”) y reduces la carga cognitiva (“no me saturas con ruido”).

Así que la próxima vez que quieras notificar algo, prueba esta pregunta simple: “¿Esto ayuda al usuario a avanzar… o solo me ayuda a mí a sentir que el sistema está hablando?”
Si ayuda a avanzar, anúncialo. Si es ruido, guárdatelo. El mejor toast, muchas veces, es el que no molesta.