Usabilidad web: migas de pan, áreas táctiles y errores que ayudan
Aplica las heurísticas de usabilidad a un formulario de contacto: migas de pan, etiquetas y autocompletado en el formulario, líneas de lectura cómodas, áreas táctiles de 44px, foco visible y una validación que explica cada error junto a su campo, lleva el foco al primero y confirma el envío.
- Heurísticas de Nielsen
- Migas de pan accesibles
- Formularios usables: label y autocomplete
- Áreas táctiles y foco visible
- Mensajes de error con aria-invalid y aria-describedby
- role="status"
Enunciado
Una web usable es la que se entiende sin pensar: el usuario sabe dónde está, lee sin esfuerzo, acierta con el dedo en los botones y, si se equivoca, sabe qué ha hecho mal y cómo arreglarlo. Son las heurísticas de Jakob Nielsen: visibilidad del estado del sistema, reconocer antes que recordar, prevenir errores y ayudar a recuperarse de ellos.
La página de contacto de una tienda en línea las incumple casi todas: unas migas de pan que son solo texto, un formulario sin etiquetas que el navegador no puede autocompletar, un «botón» que es un div, líneas de texto larguísimas en pantallas grandes y ningún mensaje cuando algo falla. Se arregla en tres pasos: HTML, CSS y JavaScript.
Qué hay que hacer
- Cada apartado se corrige por separado con pruebas que se ejecutan dentro de la página (algunas a varios anchos de pantalla). Debajo del editor tienes el resultado en directo.
- Conserva las clases, los
idy los textos visibles: las pruebas los buscan. - Los apartados se encadenan: el segundo y el tercero parten del HTML (y el tercero del CSS) ya corregidos.
Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. Dónde estoy
Las migas de pan responden a la primera pregunta del usuario. Con nav, una lista ordenada y aria-current, también la responden a quien navega con un lector de pantalla.
<nav class="migas" aria-label="Migas de pan">
<ol>
<li><a href="/">Inicio</a></li>
<li aria-current="page">Contacto</li>
</ol>
</nav>2. Leer y pulsar sin esfuerzo
Líneas de unos 65 caracteres, interlineado de 1,5 y objetivos de al menos 44px. Son números que vienen de estudios de lectura y de la ergonomía del dedo, no de gustos.
3. Errores que ayudan
Un buen mensaje de error dice qué pasa y cómo arreglarlo, aparece junto al campo, no depende solo del color y se anuncia: aria-invalid marca el campo, aria-describedby lo enlaza con su mensaje y el foco lleva al primero.
4. Confirmar lo que ha pasado
Tras enviar, el usuario necesita saber que ha funcionado. Un role="status" anuncia el mensaje al lector de pantalla sin mover el foco.
Resuélvelo aquí
Cada apartado edita un fichero de la página (el HTML, el CSS o el JavaScript) y se corrige con pruebas que se ejecutan en la propia página, también a distintos anchos de pantalla. Debajo del editor ves el resultado en directo.
Rehaz el HTML. La cabecera, en un header. Las migas de pan, en un nav con clase migas y aria-label="Migas de pan", con una lista ordenada: enlaces a / («Inicio») y /ayuda («Ayuda») y, la página actual, «Contacto» sin enlace y con aria-current="page". El formulario, con novalidate (los mensajes los pone la página): cada campo con su label for («Nombre (obligatorio)», «Correo (obligatorio)» y «Mensaje (obligatorio)»), su name, required, el autocompletado adecuado (name y email), el correo de tipo email y, detrás de cada campo, un párrafo de error vacío y oculto (class="error", id="error-nombre", error-email y error-mensaje, con hidden) enlazado desde el campo con aria-describedby. El botón, un button de tipo submit con clase boton y el texto «Enviar mensaje». Y, después del formulario, un párrafo vacío con clase estado y role="status" para avisar del envío.
Haz el CSS. Migas: la lista en fila (Flexbox con 8px de separación, sin viñetas, sin margen y con relleno de 8px y 16px), una barra / antes de cada elemento menos el primero (con ::before, 8px de margen a la derecha y color #6b7280) y los enlaces como inline-block con 12px de relleno arriba y abajo y 4px a los lados. Lectura: .contenido con 65ch de ancho máximo, 16px de relleno lateral e interlineado de 1,5. Formulario: en columna con 4px de separación; campos y botón con la fuente heredada y 44px de alto mínimo; campos con 8px de relleno y borde de 1px #6b7280. Foco visible: con :focus-visible, contorno de 3px sólido #1d4ed8 separado 2px. Errores: .error en #b91c1c sin márgenes y [aria-invalid="true"] con borde de 2px #b91c1c.
Valida el formulario con JavaScript al enviarlo (sin dejar que se envíe de verdad): el nombre necesita al menos 2 caracteres, el correo tener la forma algo@algo.algo y el mensaje al menos 10 caracteres (sin contar los espacios de los extremos). Cada campo que falle recibe aria-invalid="true" y su párrafo de error se muestra con el texto Escribe tu nombre (al menos 2 letras)., Escribe un correo válido, como nombre@ejemplo.com. o El mensaje debe tener al menos 10 caracteres.; cada campo correcto pierde el aria-invalid y su error se oculta y se vacía. Si hay errores, el foco va al primer campo con error y el estado se vacía; si no, el estado dice Mensaje enviado. Te responderemos en menos de 24 horas. y el formulario se vacía.
Solución explicada
Ver las soluciones de todos los apartados
Apartado 1 · HTML
<header class="cabecera"><a href="/">Tienda Pixel</a></header>
<nav class="migas" aria-label="Migas de pan">
<ol>
<li><a href="/">Inicio</a></li>
<li><a href="/ayuda">Ayuda</a></li>
<li aria-current="page">Contacto</li>
</ol>
</nav>
<main class="contenido">
<h1>Contacto</h1>
<p>¿Tienes una duda sobre un pedido? Escríbenos y te responderemos en menos de 24 horas laborables. Si es sobre una devolución, ten a mano el número de pedido que aparece en el correo de confirmación.</p>
<form class="formulario" novalidate>
<label for="nombre">Nombre (obligatorio)</label>
<input id="nombre" name="nombre" autocomplete="name" required aria-describedby="error-nombre">
<p class="error" id="error-nombre" hidden></p>
<label for="email">Correo (obligatorio)</label>
<input id="email" name="email" type="email" autocomplete="email" required aria-describedby="error-email">
<p class="error" id="error-email" hidden></p>
<label for="mensaje">Mensaje (obligatorio)</label>
<textarea id="mensaje" name="mensaje" required aria-describedby="error-mensaje"></textarea>
<p class="error" id="error-mensaje" hidden></p>
<button class="boton" type="submit">Enviar mensaje</button>
</form>
<p class="estado" role="status"></p>
</main>Apartado 2 · CSS
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; font-size: 16px; color: #1f2937; }
.cabecera { padding: 12px 16px; background: #1d4ed8; }
.cabecera a { color: #fff; font-weight: bold; }
/* Migas de pan en línea, separadas con una barra que no lee el lector de pantalla */
.migas ol { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; margin: 0; padding: 8px 16px; }
.migas li + li::before { content: "/"; margin-right: 8px; color: #6b7280; }
.migas a { display: inline-block; padding: 12px 4px; }
/* Lectura cómoda: líneas de 65 caracteres como mucho e interlineado de 1,5 */
.contenido { max-width: 65ch; padding: 0 16px; line-height: 1.5; }
/* Formulario: etiquetas encima, áreas táctiles de 44px como mínimo */
.formulario { display: flex; flex-direction: column; gap: 4px; }
.formulario label { font-weight: 600; margin-top: 12px; }
input, textarea { font: inherit; min-height: 44px; padding: 8px; border: 1px solid #6b7280; border-radius: 6px; }
.boton { font: inherit; min-height: 44px; padding: 0 24px; margin-top: 16px; align-self: flex-start; background: #1d4ed8; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
/* Foco siempre visible con el teclado */
:focus-visible { outline: 3px solid #1d4ed8; outline-offset: 2px; }
/* Errores: color, borde y texto, nunca solo color */
.error { margin: 0; color: #b91c1c; font-size: 0.875rem; }
[aria-invalid="true"] { border: 2px solid #b91c1c; }
.estado { font-weight: 600; color: #166534; }Apartado 3 · JavaScript
const form = document.querySelector('.formulario');
const estado = document.querySelector('.estado');
const reglas = [
['nombre', (v) => v.trim().length >= 2, 'Escribe tu nombre (al menos 2 letras).'],
['email', (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()), 'Escribe un correo válido, como nombre@ejemplo.com.'],
['mensaje', (v) => v.trim().length >= 10, 'El mensaje debe tener al menos 10 caracteres.'],
];
form.addEventListener('submit', (e) => {
e.preventDefault();
let primero = null;
for (const [id, valido, texto] of reglas) {
const campo = document.getElementById(id);
const error = document.getElementById('error-' + id);
if (valido(campo.value)) {
campo.removeAttribute('aria-invalid');
error.hidden = true;
error.textContent = '';
} else {
campo.setAttribute('aria-invalid', 'true');
error.hidden = false;
error.textContent = texto;
if (!primero) primero = campo;
}
}
if (primero) {
estado.textContent = '';
primero.focus(); // el foco va al primer error: se ve y el lector de pantalla lo lee
return;
}
estado.textContent = 'Mensaje enviado. Te responderemos en menos de 24 horas.';
form.reset();
});Cada cambio responde a una heurística: las migas, a la visibilidad del estado del sistema; el autocompletado, a reconocer antes que recordar; los mensajes concretos, a ayudar a reconocer y corregir errores; y la confirmación final, otra vez a la visibilidad del estado.
Un div con onclick parece un botón pero no lo es: no se puede alcanzar con el tabulador, no responde a Intro ni a Espacio y el lector de pantalla no lo anuncia. Un button lo hace todo sin una línea de código.
El foco visible no es un detalle estético: quien navega con el teclado necesita saber dónde está igual que quien usa el ratón ve el cursor. :focus-visible lo muestra con el teclado y no al hacer clic.
Validar al enviar y no mientras se escribe evita regañar a alguien que todavía no ha terminado; una mejora habitual es volver a validar un campo al salir de él (blur) una vez que ya ha mostrado un error.
Para ir más allá
- Valida cada campo también al salir de él (
blur), pero solo después del primer envío. - Muestra un resumen de errores al principio del formulario con enlaces a cada campo.
- Haz una prueba de usabilidad con cinco personas y una tarea («envía una queja sobre un pedido») y anota dónde dudan.