Formulario de contacto en HTML y CSS, accesible y con validación

Un formulario de contacto listo para copiar, que se llena rápido en el celular, revisa los datos antes de enviarlos y se puede usar con el teclado y con un lector de pantalla. Todo con HTML y CSS; el envío lo tiene que recibir un programa en tu servidor.

El código

<form class="contacto" action="/contacto" method="post">
  <h2>Escríbenos</h2>

  <label for="c-nombre">Nombre</label>
  <input id="c-nombre" name="nombre" autocomplete="name" required>

  <label for="c-correo">Correo electrónico</label>
  <input id="c-correo" name="correo" type="email" autocomplete="email" required
         aria-describedby="c-correo-ayuda">
  <small id="c-correo-ayuda">Solo lo usaremos para responderte.</small>

  <label for="c-tel">Teléfono <span class="opcional">(opcional)</span></label>
  <input id="c-tel" name="telefono" type="tel" autocomplete="tel"
         inputmode="tel" pattern="[0-9 +\(\)\-]{8,20}">

  <fieldset>
    <legend>Motivo</legend>
    <label><input type="radio" name="motivo" value="cotizacion" required> Cotización</label>
    <label><input type="radio" name="motivo" value="soporte"> Soporte</label>
    <label><input type="radio" name="motivo" value="otro"> Otro</label>
  </fieldset>

  <label for="c-mensaje">Mensaje</label>
  <textarea id="c-mensaje" name="mensaje" rows="5" minlength="20" maxlength="2000" required></textarea>

  <label class="casilla">
    <input type="checkbox" name="aviso" required>
    Acepto el <a href="#">aviso de privacidad</a>
  </label>

  <button>Enviar mensaje</button>
</form>
.contacto {
  display: grid;
  gap: 6px;
  max-width: 28rem;
  font-family: system-ui, sans-serif;
}
.contacto h2 { margin: 0 0 6px; }
.contacto label { font-weight: 600; margin-top: 8px; }
.contacto input, .contacto textarea, .contacto button { font: inherit; }
.contacto input:not([type=radio], [type=checkbox]), .contacto textarea {
  padding: 8px 10px;
  border: 2px solid #c9d6e4;
  border-radius: 8px;
}
.contacto :focus-visible { outline: 3px solid #1e64af; outline-offset: 1px; }
.contacto :user-invalid { border-color: #b91c1c; background: #fdf2f2; }
.contacto small, .opcional { color: #5b6b7c; font-weight: 400; }
.contacto fieldset { border: 1px solid #c9d6e4; border-radius: 8px; margin-top: 8px; }
.contacto fieldset label, .casilla { font-weight: 400; display: block; margin-top: 4px; }
.contacto button {
  margin-top: 12px;
  padding: 10px;
  border: 0;
  border-radius: 8px;
  background: #1e64af;
  color: white;
  font-weight: 700;
  cursor: pointer;
}

Pulsa «Enviar mensaje» con el formulario vacío y el navegador te llevará al primer campo que falta. Comprobado en los tres navegadores: vacío no se envía, bien lleno sí, y un teléfono con letras no pasa el pattern. El marco del resultado te enseña los datos que se habrían enviado.

Qué hace cada parte

  • Cada campo tiene su label unido por for e id. El texto de ayuda del correo se une con aria-describedby, y así el lector de pantalla lo lee después de la etiqueta.
  • autocomplete (name, email, tel) deja que el navegador rellene con los datos guardados.
  • type="email" y type="tel" sacan el teclado adecuado en el celular. El teléfono es opcional, pero si se escribe tiene que cumplir el pattern. Ojo con los paréntesis y el guion dentro de los corchetes: los navegadores actuales leen el pattern con el modo v de las expresiones regulares, que exige escaparlos (\(, \), \-). Sin las barras, la expresión es inválida y el navegador ignora el pattern sin avisar: nos pasó al preparar este ejemplo, y lo detectó la prueba con «abc».
  • Los botones de opción comparten name y van en un fieldset con su legend; con poner required en uno, el grupo entero es obligatorio.
  • :user-invalid marca en rojo los campos mal llenados solo después de tocarlos, no al abrir la página.
  • method="post": el mensaje no debe quedar en la dirección ni en el historial.
La validación del navegador es comodidad, no seguridad: cualquiera puede enviar datos a tu servidor sin pasar por este formulario. El programa que lo recibe tiene que volver a revisarlo todo, limitar los envíos para frenar el spam y escapar el texto antes de mostrarlo o guardarlo. En PHP, lo explicamos en seguridad en PHP.

Cada fragmento de HTML de esta página pasó por el Nu Html Checker 26.11.4 (el validador del W3C) y cada resultado se pintó y se midió en Chromium, Firefox y WebKit antes de publicarla. Prueba tus cambios en el editor de HTML y CSS o aprende desde cero con el curso.