La mayoría de los formularios HTML que encuentro en producción utilizan tres tipos de input: text, password y submit. Solo eso. Mientras tanto, HTML lleva años teniendo tipos de input enriquecidos, validación por restricciones y características de accesibilidad — todo disponible sin JavaScript y sin npm install. Vamos a usarlos.

Tipos de input que vale la pena conocer

Cada tipo de input hace trabajo real por ti: activa el teclado móvil correcto, proporciona validación integrada y comunica la intención a las tecnologías de asistencia. Estos son los que aparecen constantemente en proyectos reales:

html
<!-- Email: validates format, shows email keyboard on mobile -->
<input type="email" name="email" autocomplete="email">

<!-- Phone: shows numeric keypad on mobile -->
<input type="tel" name="phone" autocomplete="tel" pattern="[0-9]{10,15}">

<!-- Number: spin buttons, min/max/step validation -->
<input type="number" name="quantity" min="1" max="99" step="1" value="1">

<!-- Date: native date picker (no library needed) -->
<input type="date" name="birthdate" min="1900-01-01" max="2026-12-31">

<!-- Range: slider with min/max/step -->
<input type="range" name="volume" min="0" max="100" step="5" value="50">

<!-- Color: native color picker -->
<input type="color" name="theme_color" value="#0066cc">

<!-- File: single or multiple file upload -->
<input type="file" name="resume" accept=".pdf,.doc,.docx">
<input type="file" name="photos" accept="image/*" multiple>
Consejo profesional: type="date" devuelve el valor en formato YYYY-MM-DD independientemente de la configuración regional del usuario — que es exactamente lo que quieres al enviar a un servidor. Nunca parsees fechas de inputs type="text" si puedes evitarlo.

Asociación de labels — hazlo bien

Los labels no son decoración opcional. Son la forma principal en que los lectores de pantalla identifican los inputs, y hacer clic en un label debe enfocar el input asociado. Hay dos enfoques válidos:

html
<!-- Method 1: for/id association (most common, most flexible) -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">

<!-- Method 2: wrapping label (no id needed) -->
<label>
  Email address
  <input type="email" name="email">
</label>

<!-- Wrong: placeholder is NOT a label -->
<input type="email" name="email" placeholder="Email address">
<!-- placeholder disappears when the user starts typing — terrible UX for screen readers -->

El enfoque for/id generalmente es preferible porque mantiene los labels e inputs desacoplados en el DOM, lo que es más fácil de estilar. Nunca uses placeholder como sustituto de un label — falla a los usuarios con discapacidades cognitivas y desaparece en el momento en que alguien empieza a escribir. El tutorial de labels del W3C WAI y la guía de controles de formulario de WebAIM cubren con más profundidad la justificación de accesibilidad.

Validación por restricciones integrada

La validación por restricciones integrada de HTML se ejecuta antes del envío del formulario y es genuinamente potente. Obtienes muchos comportamientos útiles de forma gratuita:

html
<form>
  <!-- required: field must have a value -->
  <label for="fullname">Full name</label>
  <input type="text" id="fullname" name="fullname" required>

  <!-- minlength/maxlength: character count constraints -->
  <label for="username">Username (3–20 chars)</label>
  <input type="text" id="username" name="username"
         minlength="3" maxlength="20" required>

  <!-- pattern: regex-based validation -->
  <label for="postcode">UK Postcode</label>
  <input type="text" id="postcode" name="postcode"
         pattern="[A-Z]{1,2}[0-9][0-9A-Z]?s?[0-9][A-Z]{2}"
         title="Enter a valid UK postcode (e.g. SW1A 1AA)"
         required>

  <!-- min/max on date -->
  <label for="checkin">Check-in date</label>
  <input type="date" id="checkin" name="checkin"
         min="2026-04-16" required>

  <button type="submit">Submit</button>
</form>
  • required. El campo debe tener un valor. En checkboxes, debe estar marcado.
  • pattern. Una regex a la que el valor debe coincidir. Agrega siempre un atributo title — se convierte en el texto del tooltip de validación y da a los usuarios una pista sobre el formato esperado.
  • minlength / maxlength. Recuento de caracteres para inputs de texto. maxlength trunca silenciosamente la entrada; minlength solo valida al enviar.
  • min / max. Límites numéricos o de fecha. Funciona en inputs number, date, range y time.
  • step. Define los incrementos válidos. step="0.01" en un campo de moneda permite céntimos. step="any" desactiva completamente la comprobación de paso.

fieldset y legend para agrupar

Cuando tienes un grupo de inputs relacionados — especialmente botones de radio o checkboxes — <fieldset> y <legend> los agrupan semánticamente. Los lectores de pantalla leen el legend antes de cada input del grupo, por lo que los usuarios siempre saben qué pregunta responde un botón de radio.

html
<form>
  <fieldset>
    <legend>Preferred contact method</legend>

    <label>
      <input type="radio" name="contact" value="email" checked>
      Email
    </label>
    <label>
      <input type="radio" name="contact" value="phone">
      Phone
    </label>
    <label>
      <input type="radio" name="contact" value="post">
      Post
    </label>
  </fieldset>

  <fieldset>
    <legend>Notification preferences</legend>

    <label>
      <input type="checkbox" name="notify_new_posts" value="1">
      New articles
    </label>
    <label>
      <input type="checkbox" name="notify_replies" value="1">
      Replies to my comments
    </label>
  </fieldset>
</form>

La API de validación por restricciones

La API de validación por restricciones te da acceso JavaScript a la misma lógica de validación que el navegador usa de forma nativa. Esto te permite activar la validación programáticamente y establecer mensajes de error personalizados sin bloquear el envío:

js
const usernameInput = document.getElementById('username');

// Check if a single field is valid
console.log(usernameInput.checkValidity()); // true or false
console.log(usernameInput.validity.tooShort); // true if below minlength
console.log(usernameInput.validity.patternMismatch); // true if pattern fails

// Set a custom error message (shows in the browser's native validation tooltip)
usernameInput.setCustomValidity('That username is already taken.');
usernameInput.reportValidity(); // triggers the tooltip immediately

// Clear a custom error (important — once set, it sticks until cleared)
usernameInput.setCustomValidity('');

// Validate on the fly as the user types (async example — username availability)
usernameInput.addEventListener('input', async () => {
  const value = usernameInput.value;
  if (value.length < 3) return; // let minlength handle this

  const response = await fetch(`/api/check-username?q=${encodeURIComponent(value)}`);
  const { available } = await response.json();

  usernameInput.setCustomValidity(available ? '' : 'Username is already taken.');
});

Fíjate en el detalle crítico: una vez que llamas a setCustomValidity() con una cadena no vacía, el campo es permanentemente inválido hasta que lo limpies llamando a setCustomValidity(''). Olvidar el paso de limpieza es el bug más común al usar esta API.

novalidate para validación JS personalizada

Si estás construyendo una UI de validación personalizada con mensajes de error con estilo (no los tooltips nativos del navegador), añade novalidate al formulario. Esto deshabilita la validación nativa del navegador mientras mantiene disponible la API de validación por restricciones para tus propias comprobaciones:

html
<form id="signup-form" novalidate>
  <div class="field">
    <label for="signup-email">Email</label>
    <input type="email" id="signup-email" name="email" required>
    <span class="error" aria-live="polite"></span>
  </div>

  <div class="field">
    <label for="signup-password">Password</label>
    <input type="password" id="signup-password" name="password" minlength="8" required>
    <span class="error" aria-live="polite"></span>
  </div>

  <button type="submit">Create account</button>
</form>
js
const form = document.getElementById('signup-form');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  let isValid = true;

  form.querySelectorAll('input').forEach((input) => {
    const errorEl = input.nextElementSibling;

    if (!input.checkValidity()) {
      isValid = false;
      errorEl.textContent = input.validationMessage;
      input.setAttribute('aria-invalid', 'true');
    } else {
      errorEl.textContent = '';
      input.removeAttribute('aria-invalid');
    }
  });

  if (isValid) {
    form.submit();
  }
});

autocomplete y aria-describedby

Dos atributos que mejoran significativamente la UX de los formularios y son fáciles de pasar por alto:

html
<!-- autocomplete: tells browsers/password managers what the field is for -->
<input type="text"     name="fname"    autocomplete="given-name">
<input type="text"     name="lname"    autocomplete="family-name">
<input type="email"    name="email"    autocomplete="email">
<input type="tel"      name="phone"    autocomplete="tel">
<input type="password" name="password" autocomplete="current-password">
<input type="password" name="new_pass" autocomplete="new-password">
<input type="text"     name="cc"       autocomplete="cc-number">

<!-- aria-describedby: links an input to a hint or error message -->
<label for="pass">Password</label>
<input
  type="password"
  id="pass"
  name="password"
  minlength="8"
  aria-describedby="pass-hint"
  required
>
<span id="pass-hint">Must be at least 8 characters.</span>

El atributo autocomplete usa valores de token estandarizados definidos por la especificación WHATWG. Cuando usas el token correcto, los navegadores y los gestores de contraseñas pueden rellenar automáticamente los campos de forma fiable. aria-describedby asocia un mensaje de ayuda o de error con un input — los lectores de pantalla leen la descripción después del label del campo, lo que hace que las restricciones sean audibles antes de que el usuario empiece a escribir.

Un formulario de inicio de sesión accesible completo

Aquí está todo combinado — un formulario de inicio de sesión que funciona para usuarios de teclado, usuarios de lector de pantalla y usuarios móviles, usando únicamente HTML y una pequeña cantidad de JavaScript:

html
<form id="login-form" novalidate>
  <h2>Sign in</h2>

  <div class="field">
    <label for="login-email">Email address</label>
    <input
      type="email"
      id="login-email"
      name="email"
      autocomplete="email"
      aria-describedby="email-error"
      required
    >
    <span id="email-error" class="error" aria-live="polite" role="alert"></span>
  </div>

  <div class="field">
    <label for="login-password">Password</label>
    <input
      type="password"
      id="login-password"
      name="password"
      autocomplete="current-password"
      aria-describedby="password-error"
      required
    >
    <span id="password-error" class="error" aria-live="polite" role="alert"></span>
    <a href="/forgot-password">Forgot password?</a>
  </div>

  <label class="inline">
    <input type="checkbox" name="remember" value="1">
    Keep me signed in for 30 days
  </label>

  <button type="submit">Sign in</button>
</form>

Puntos clave: aria-live="polite" en los spans de error significa que los lectores de pantalla anunciarán los errores cuando aparezcan sin interrumpir lo que el usuario esté escuchando en ese momento. role="alert" refuerza esto para lectores de pantalla más antiguos. Los valores de autocomplete coinciden con lo que esperan los gestores de contraseñas, por lo que el autocompletado funciona correctamente en el primer intento.

Herramientas útiles

Al construir y probar formularios HTML, el Validador HTML detecta errores estructurales como labels faltantes e IDs duplicados antes de que los usuarios los encuentren. Para la limpieza general del HTML, el Formateador HTML mantiene tu marcado legible. La referencia del elemento input en MDN es la documentación más completa para todos los tipos de input y sus atributos.

Conclusión

Los formularios HTML tienen mucha más capacidad integrada de la que la mayoría de los proyectos aprovecha. Elegir el tipo de input correcto te proporciona teclados móviles, validación y significado semántico de forma gratuita. La asociación correcta de labels y aria-describedby hace que los formularios sean navegables sin ratón. La API de validación por restricciones te da hooks JavaScript en la validación nativa sin pelearte con el navegador. Une estas piezas y obtienes formularios que funcionan para todos — antes de escribir una sola línea de lógica de validación personalizada.