Por qué los formularios merecen atención
Los formularios son el lugar donde los usuarios te pagan, se registran, realizan búsquedas y envían mensajes. También es donde se concentra la frustración: una etiqueta faltante, un error confuso o un input que abre el teclado incorrecto pueden hacer que pierdas a un usuario en segundos.
La buena noticia es que HTML ya resuelve la mayor parte de esto. Con etiquetas adecuadas, los tipos de input correctos y restricciones nativas, obtienes formularios accesibles, optimizados para móviles y validados antes de escribir una sola línea de JavaScript.
Estructura del formulario
Un formulario envuelve sus controles y declara dónde y cómo enviarlos.
<!-- signup.html -->
<form action="/signup" method="post">
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="password">Password</label>
<input id="password" name="password" type="password" minlength="8" required />
<button type="submit">Create account</button>
</form>
- action es la URL que recibe los datos.
- method es
getopost. - name en cada control es la clave en los datos enviados.
- type=“submit” activa el envío; un botón simple dentro de un formulario actúa como submit por defecto, así que usa
type="button"para otras acciones.
Etiquetas
Cada control necesita una etiqueta. Las etiquetas son anunciadas por los lectores de pantalla, actúan como un área de clic más amplia y permanecen visibles después de que el campo ha sido completado.
<!-- label.html -->
<label for="phone">Phone number</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" />
También puedes envolver el control dentro de la etiqueta, lo que los asocia implícitamente:
<label>
Subscribe to the newsletter
<input type="checkbox" name="subscribe" />
</label>
Los placeholders no son etiquetas. Desaparecen una vez que el usuario escribe, suelen tener un contraste deficiente y no se anuncian de manera fiable. Utiliza una etiqueta real y, si es útil, una pista (hint).
Tipos de input
Elegir el type adecuado proporciona a los usuarios el teclado móvil correcto y permite habilitar la validación nativa.
| Tipo | Uso | Beneficio |
|---|---|---|
| Direcciones de correo electrónico | Teclado de email, verificación de formato | |
| tel | Números de teléfono | Teclado numérico |
| number | Valores numéricos | Entrada numérica, min/max/step |
| url | Direcciones web | Teclado de URL, verificación de formato |
| date | Fechas | Selector de fecha |
| password | Secretos | Entrada enmascarada |
| search | Campos de búsqueda | Teclado de búsqueda, botón de limpiar |
| file | Subidas de archivos | Selector de archivos y filtrado por tipo |
Utiliza autocomplete con tokens estándar como email, given-name, postal-code y new-password para que los navegadores y los gestores de contraseñas puedan ayudar al usuario.
Agrupación de controles
Agrupa los controles relacionados para que su relación sea clara para todos.
<!-- address.html -->
<fieldset>
<legend>Shipping address</legend>
<label for="street">Street</label>
<input id="street" name="street" autocomplete="address-line1" />
<label for="city">City</label>
<input id="city" name="city" autocomplete="address-level2" />
</fieldset>
fieldset y legend agrupan botones de radio y campos relacionados, los cuales los lectores de pantalla anuncian como un conjunto. Para los grupos de radio, utiliza siempre un fieldset con un legend.
Validación nativa
HTML ofrece validación de restricciones (constraint validation) a través de atributos.
<!-- validate.html -->
<input type="email" name="email" required />
<input type="password" name="password" minlength="8" required />
<input type="number" name="age" min="18" max="120" step="1" />
<input type="text" name="handle" pattern="[a-z0-9_]{3,15}" />
<input type="url" name="website" />
El navegador bloquea el envío y muestra un mensaje cuando una restricción falla. Puedes dar estilo a estos estados con CSS:
/* styles.css */
input:invalid { border-color: #dc2626; }
input:valid { border-color: #16a34a; }
input:focus-visible { outline: 2px solid #2563eb; }
Los mensajes nativos son funcionales, pero breves y no están traducidos. Para formularios importantes, proporciona tus propios mensajes claros utilizando la Constraint Validation API:
// validate.js
const email = document.querySelector("#email");
email.addEventListener("invalid", () => {
email.setCustomValidity("");
if (email.validity.valueMissing) {
email.setCustomValidity("Please enter your email address.");
} else if (email.validity.typeMismatch) {
email.setCustomValidity("That does not look like an email address.");
}
});
email.addEventListener("input", () => email.setCustomValidity(""));
Recuerda limpiar el mensaje personalizado al escribir en el input, o de lo contrario el campo seguirá siendo inválido.
Errores accesibles
La validación solo es útil si los usuarios entienden qué salió mal.
- Muestra un mensaje de texto claro junto al campo, no solo un borde rojo.
- Asócialo con el control utilizando
aria-describedby. - Marca el campo con
aria-invalid="true". - Mueve el foco al primer campo inválido al enviar el formulario.
- Resume los errores en la parte superior en formularios largos.
<!-- error.html -->
<label for="email">Email address</label>
<input id="email" name="email" type="email" required
aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" class="error">Please enter a valid email address.</p>
Nunca dependas únicamente del color; combínalo con texto y un icono.
Valida siempre en el servidor
La validación en el lado del cliente es una funcionalidad de experiencia de usuario, no un control de seguridad. Cualquier persona puede editar la página, desactivar JavaScript o enviar una solicitud directamente. El servidor debe validar los tipos, longitudes, formatos y permisos, y devolver errores claros que el cliente pueda mostrar. Consulta la guía de Web Security.
Mejores prácticas
- Asigna a cada control una etiqueta visible y asociada.
- Utiliza el tipo de input correcto y tokens
autocomplete. - Agrupa los controles relacionados con
fieldsetylegend. - Prioriza las restricciones nativas y luego mejóralas con la Constraint Validation API.
- Muestra mensajes de error específicos y accesibles cerca del campo.
- Valida nuevamente en el servidor en cada solicitud.
- Mantén los formularios cortos y solicita únicamente lo necesario.
Errores comunes
- Usar placeholders en lugar de labels.
- Olvidar los atributos
name, lo que provoca que los campos no se envíen. - Enviar datos sensibles mediante GET.
- Confiar únicamente en la validación del lado del cliente para la seguridad.
- Comunicar errores basándose solo en el color.
- Reiniciar todo el formulario ante un error de validación, provocando la pérdida de los datos ingresados.
Próximos pasos
Los formularios combinan marcado, accesibilidad y validación. Refuerza el marcado con Semantic HTML, hazlo inclusivo con la guía de accesibilidad y gestiona los envíos con JavaScript. Después, audita un formulario de tu aplicación utilizando la lista de verificación anterior.