Un formulario accesible se arma de a pares: cada control tiene una etiqueta asociada, cada error se anuncia, y todo funciona sin mouse. Tailwind no toca ese comportamiento — solo lo viste.
label + for/id: la asociación obligatoria
Sin un label correctamente asociado, un lector de pantalla anuncia el input como 'edit text' sin más contexto — y hacer click en el texto no enfoca el campo.
→El id del input y el for del label deben coincidir exactamente. Alternativa: envolver el input dentro del label y omitir ambos atributos.
→Un placeholder nunca reemplaza al label — desaparece al escribir y muchos lectores de pantalla lo ignoran.
Tipos de input semánticos
El atributo type no solo valida formato: en mobile cambia el teclado que aparece (numérico para tel, con @ visible para email), y el navegador puede ofrecer autocompletado apropiado.
→type='number' tiene comportamientos raros (flechas, scroll cambia el valor) — para códigos postales o números de tarjeta suele ser mejor type='text' inputmode='numeric'.
fieldset + legend: agrupar controles relacionados
fieldset dibuja un grupo semántico (por ejemplo, un set de radios) y legend le da un título. Los lectores de pantalla anuncian la legend antes de cada opción del grupo.
→Todos los radios de un mismo grupo deben compartir el mismo name — así el navegador sabe que son mutuamente excluyentes.
Estado de error accesible
Un borde rojo no es suficiente: alguien con daltonismo o usando un lector de pantalla necesita el mensaje de error asociado explícitamente vía aria-describedby, y el campo marcado con aria-invalid.
⚠ Debe tener al menos 8 caracteres.
<label for="pw" class="block text-sm font-medium mb-1">Contraseña</label>
<input
id="pw"
type="password"
aria-invalid="true"
aria-describedby="pw-error"
class="w-full rounded-lg border-2 border-rose-500 px-3 py-2"
/>
<p id="pw-error" class="text-sm text-rose-600 mt-1">
Debe tener al menos 8 caracteres.
</p>
→aria-describedby apunta al id del mensaje de error — el lector de pantalla lo lee inmediatamente después del campo.
→El color no debería ser la única señal: por eso conviene sumar un ícono o texto, no solo border-rose-500.
button type: submit vs button vs reset
Dentro de un <form>, un <button> sin type explícito actúa como submit por defecto — una fuente clásica de bugs cuando quieres un botón que no envíe el formulario (ej: 'mostrar contraseña').