Formularios en HTML: campos, etiquetas y validación

Los formularios en HTML son la parte de una página que habla en las dos direcciones: el buscador, el registro, el carrito de compras. En esta lección harás formularios que se entienden, que se llenan rápido en el celular y que revisan los datos antes de enviarlos, sin escribir nada de JavaScript.

Un formulario mínimo

Un formulario es la única forma, sin JavaScript, de que una página reciba datos de quien la visita: un buscador, un registro, un pedido, un comentario. Todo va dentro de un elemento <form>, y cada dato se escribe en un campo. El más común es <input>, un elemento vacío que cambia de forma según su atributo type.

<form action="/buscar" method="get">
  <label for="q">Buscar en el sitio</label>
  <input id="q" name="q" type="search">
  <button>Buscar</button>
</form>

Escribe algo y pulsa «Buscar»: el marco del resultado te enseña lo que se habría enviado. Tres piezas hacen que funcione:

  • action dice a qué dirección se envían los datos y method, cómo (lo verás enseguida).
  • name="q" es el nombre con el que viaja el dato. Un campo sin name no se envía, por mucho que el usuario escriba en él.
  • <label for="q"> es la etiqueta visible del campo y se une a él por su id. Haz clic en el texto «Buscar en el sitio»: el cursor salta al campo.

Las etiquetas, siempre

Cada campo necesita su <label>. Además del clic (en el celular, una zona más grande para tocar), es lo que lee un lector de pantalla al llegar al campo; sin ella diría solo «editar texto». Hay dos formas de unirlos, y las dos valen:

<!-- Con for e id -->
<label for="correo">Correo</label>
<input id="correo" name="correo" type="email">

<!-- Envolviendo el campo -->
<label>
  Teléfono
  <input name="telefono" type="tel">
</label>

El texto de ejemplo dentro del campo (placeholder) no sustituye a la etiqueta: desaparece al escribir, suele tener poco contraste y muchos lectores de pantalla no lo leen. Úsalo, si acaso, para un ejemplo del formato: placeholder="55 1234 5678".

El error típico: una etiqueta que apunta a nada

<label for="nombre">Nombre</label>
<input id="name" name="nombre">
Nu Html Checker (validador del W3C)línea 1: error: The value of the “for” attribute of the “label” element must be the ID of a non-hidden form control.

El for dice nombre y el id del campo es name: la etiqueta no está unida a nada, el clic no hace nada y el lector de pantalla no la lee. Pasa mucho al traducir o copiar formularios. El for y el id tienen que ser idénticos, mayúsculas incluidas.

Los tipos de campo

Elegir bien el type cambia mucho la experiencia, sobre todo en el celular, que muestra el teclado adecuado: con arroba para el correo, numérico para el teléfono, un calendario para la fecha.

<form>
  <label for="t1">Nombre (text)</label>
  <input id="t1" name="nombre" type="text" autocomplete="name">

  <label for="t2">Correo (email)</label>
  <input id="t2" name="correo" type="email" autocomplete="email">

  <label for="t3">Teléfono (tel)</label>
  <input id="t3" name="tel" type="tel" autocomplete="tel">

  <label for="t4">Contraseña (password)</label>
  <input id="t4" name="clave" type="password" autocomplete="new-password">

  <label for="t5">Personas (number)</label>
  <input id="t5" name="personas" type="number" min="1" max="12" value="2">

  <label for="t6">Fecha de llegada (date)</label>
  <input id="t6" name="llegada" type="date">

  <label for="t7">Color favorito (color)</label>
  <input id="t7" name="color" type="color" value="#1e64af">

  <label for="t8">Volumen (range)</label>
  <input id="t8" name="volumen" type="range" min="0" max="10">

  <button>Enviar</button>
</form>
body { font-family: system-ui, sans-serif; padding: 0.5rem 1rem; }
form { display: grid; gap: 0.3rem; max-width: 22rem; }
input, select, textarea, button { font: inherit; padding: 0.45rem; }
label { margin-top: 0.4rem; font-weight: 600; }

El atributo autocomplete le dice al navegador qué dato es (name, email, tel, street-address, postal-code…), para que pueda rellenarlo solo con los datos guardados. Ahorra mucho tecleo en el celular y los gestores de contraseñas lo necesitan para ofrecer una clave segura.

Elegir entre opciones

Para escoger una sola opción de pocas, botones de opción (radio); de muchas, una lista desplegable (<select>). Para marcar varias, casillas (checkbox). Los grupos de opciones se envuelven en un <fieldset> con su título en <legend>, que el lector de pantalla anuncia al entrar al grupo.

<form>
  <fieldset>
    <legend>Tamaño del café</legend>
    <label><input type="radio" name="tamano" value="chico"> Chico</label>
    <label><input type="radio" name="tamano" value="mediano" checked> Mediano</label>
    <label><input type="radio" name="tamano" value="grande"> Grande</label>
  </fieldset>

  <fieldset>
    <legend>Extras</legend>
    <label><input type="checkbox" name="extra" value="canela"> Canela</label>
    <label><input type="checkbox" name="extra" value="crema"> Crema batida</label>
  </fieldset>

  <label for="sucursal">Sucursal</label>
  <select id="sucursal" name="sucursal">
    <option value="">Elige una</option>
    <option value="cdmx">Ciudad de México</option>
    <option value="gdl">Guadalajara</option>
    <option value="mty">Monterrey</option>
  </select>

  <label for="nota">Indicaciones</label>
  <textarea id="nota" name="nota" rows="3"></textarea>

  <button>Pedir</button>
</form>
body { font-family: system-ui, sans-serif; padding: 0.5rem 1rem; }
form { display: grid; gap: 0.3rem; max-width: 22rem; }
input, select, textarea, button { font: inherit; padding: 0.45rem; }
label { margin-top: 0.4rem; font-weight: 600; }
fieldset { border: 1px solid #c9d6e4; border-radius: 8px; }

Lo que se envía de cada opción es su value, no el texto que ve el usuario: «Mediano» viaja como tamano = mediano. Pruébalo marcando dos extras: llegan los dos con el mismo nombre.

El error típico: los botones de opción se pueden marcar todos

<p>¿Con hielo?</p>
<label><input type="radio" name="hielo-si"> Sí</label>
<label><input type="radio" name="hielo-no"> No</label>

Haz clic en las dos opciones: quedan marcadas a la vez (lo comprobamos en los tres navegadores: tras pulsar «Sí» y luego «No», siguen marcadas las dos). Los botones de opción solo se excluyen entre sí cuando comparten el mismo name; aquí cada uno tiene el suyo. Lo correcto es name="hielo" en los dos, con value="si" y value="no".

Los botones

Un <button> dentro de un formulario es, si no dices otra cosa, de tipo submit: lo envía. Para un botón que hace otra cosa (mostrar la contraseña, sumar una cantidad) hay que escribir type="button", o el formulario se enviará cada vez que lo pulsen. El tipo reset, que borra todo lo escrito, casi nunca es buena idea: es muy fácil pulsarlo por error.

Validación del navegador

Antes de enviar, el navegador puede revisar los datos y avisar de lo que está mal, sin una línea de JavaScript. Se pide con atributos:

  • requiredEl campo no puede ir vacío.
  • type="email", "url"Tiene que tener forma de correo o de dirección web.
  • minlength, maxlengthNúmero mínimo y máximo de caracteres.
  • min, max, stepLímites de números y fechas, y de cuánto en cuánto.
  • patternUna expresión regular que tiene que cumplir todo el texto, por ejemplo cinco dígitos para un código postal.
<form>
  <label for="v1">Correo</label>
  <input id="v1" name="correo" type="email" required>

  <label for="v2">Código postal</label>
  <input id="v2" name="cp" inputmode="numeric" pattern="[0-9]{5}"
         title="Cinco dígitos, por ejemplo 06700">

  <label for="v3">Edad</label>
  <input id="v3" name="edad" type="number" min="18" max="120">

  <button>Registrarme</button>
</form>
body { font-family: system-ui, sans-serif; padding: 0.5rem 1rem; }
form { display: grid; gap: 0.3rem; max-width: 22rem; }
input, select, textarea, button { font: inherit; padding: 0.45rem; }
label { margin-top: 0.4rem; font-weight: 600; }
input:user-invalid {
  border: 2px solid #b91c1c;
  background: #fdf2f2;
}

Pulsa «Registrarme» con los campos vacíos o mal llenados y verás el aviso del navegador, en el idioma de tu sistema. Algunas cosas que comprobamos en los tres navegadores con este mismo formulario:

  • El correo ana no pasa, pero ana@ejemplo sí (true): para la especificación de HTML basta con una arroba y algo detrás, sin punto. Si necesitas más, revísalo en el servidor.
  • Un código postal de cuatro dígitos no cumple el pattern, y una edad de 17 queda por debajo de min.
  • El atributo title del código postal se añade al mensaje de error: úsalo para explicar el formato.

La regla de CSS :user-invalid pinta de rojo los campos con errores, pero solo después de que el usuario los haya tocado o haya intentado enviar. Su prima :invalid se aplica desde el principio, y un formulario recién abierto que ya sale lleno de rojo asusta a cualquiera. :user-invalid funciona en Chrome y Edge desde la versión 119, en Firefox desde la 88 y en Safari desde la 16.5.

La validación del navegador es una ayuda, no una protección. Cualquiera puede saltársela: desactivándola desde las herramientas del navegador o enviando la petición directamente, sin pasar por tu página. El servidor que recibe los datos tiene que volver a revisarlo todo, siempre. Lo verás en la práctica en las lecciones de PHP.

GET o POST

El atributo method decide cómo viajan los datos:

Un formulario con nombre Ana y ciudad León. Con GET los datos van en la dirección; con POST van dentro de la peticiónQué viaja al enviar el formularioCampos: nombre = Ana, ciudad = León<form action="/registro">method=“get”: en la dirección/registro?nombre=Ana&ciudad=Le%C3%B3nSe ve, se guarda en el historial: búsquedas.method=“post”: dentro de la peticiónPOST /registronombre=Ana&ciudad=Le%C3%B3nNo sale en la dirección: altas, compras, claves.
Con GET los datos se ven en la dirección; con POST van dentro de la petición. Esquema: decodigo.com.
  • GET (el que se usa si no pones nada): los datos se añaden a la dirección después de un ?. Úsalo cuando el formulario solo consulta: un buscador, unos filtros. La ventaja es que el resultado se puede guardar en favoritos o compartir.
  • POST: los datos viajan dentro de la petición, no en la dirección. Úsalo cuando el formulario cambia algo (crear una cuenta, comprar, publicar un comentario) y siempre que haya contraseñas o datos personales, que no deben quedar en el historial ni en los registros de los servidores.

Para subir archivos hacen falta las dos cosas juntas: method="post" y enctype="multipart/form-data" en el form, con un <input type="file">. Y recuerda que POST no cifra nada: lo que protege los datos por el camino es que la página use HTTPS.

Retos

Reto 1. Haz un formulario de contacto con nombre (obligatorio), correo (obligatorio, tipo correo), un asunto a elegir entre «Duda», «Queja» y «Otro», y un mensaje de al menos 20 caracteres. Que se envíe por POST.

Ver la solución
<form action="/contacto" method="post">
  <label for="r-nombre">Nombre</label>
  <input id="r-nombre" name="nombre" autocomplete="name" required>

  <label for="r-correo">Correo</label>
  <input id="r-correo" name="correo" type="email" autocomplete="email" required>

  <label for="r-asunto">Asunto</label>
  <select id="r-asunto" name="asunto">
    <option value="duda">Duda</option>
    <option value="queja">Queja</option>
    <option value="otro">Otro</option>
  </select>

  <label for="r-mensaje">Mensaje</label>
  <textarea id="r-mensaje" name="mensaje" rows="4" minlength="20" required></textarea>

  <button>Enviar</button>
</form>
body { font-family: system-ui, sans-serif; padding: 0.5rem 1rem; }
form { display: grid; gap: 0.3rem; max-width: 22rem; }
input, select, textarea, button { font: inherit; padding: 0.45rem; }
label { margin-top: 0.4rem; font-weight: 600; }

Reto 2. Escribe un campo para un RFC de persona física: cuatro letras, seis dígitos y tres caracteres que pueden ser letras o dígitos (13 en total, en mayúsculas). Usa pattern y un title que explique el formato.

Ver la solución
<label for="rfc">RFC</label>
<input id="rfc" name="rfc" maxlength="13"
       pattern="[A-ZÑ&]{4}[0-9]{6}[A-Z0-9]{3}"
       title="13 caracteres en mayúsculas, por ejemplo GODE561231GR8">

Con GODE561231GR8 el campo es válido (true) y en minúsculas no (false): pattern distingue mayúsculas. Es solo una revisión de forma; para comprobar de verdad un RFC está la herramienta calcular el RFC.

Reto 3. Este formulario tiene cuatro problemas. ¿Cuáles? <form><input placeholder="Correo"><input type="password" name="clave"><button type="button">Entrar</button></form>

Ver la solución

1) Los campos no tienen label; el placeholder no cuenta. 2) El correo no tiene name, así que no se enviaría, ni type="email". 3) El botón es type="button": no envía el formulario. 4) Sin method="post" se usaría GET y la contraseña acabaría en la dirección y en el historial.

<form action="/entrar" method="post">
  <label for="e-correo">Correo</label>
  <input id="e-correo" name="correo" type="email" autocomplete="email" required>
  <label for="e-clave">Contraseña</label>
  <input id="e-clave" name="clave" type="password" autocomplete="current-password" required>
  <button>Entrar</button>
</form>
body { font-family: system-ui, sans-serif; padding: 0.5rem 1rem; }
form { display: grid; gap: 0.3rem; max-width: 22rem; }
input, select, textarea, button { font: inherit; padding: 0.45rem; }
label { margin-top: 0.4rem; font-weight: 600; }

Cada fragmento de HTML de esta lección pasó por el Nu Html Checker 26.11.4, el validador oficial del W3C, y cada resultado se pintó en Chromium, Firefox y WebKit (los motores de Chrome, Firefox y Safari); las cifras del texto se midieron en los tres. ¿Quieres probar algo? Usa el editor de HTML y CSS. Logotipos de HTML5 (W3C) y CSS3 (Rudloff) en la imagen de la página: Wikimedia Commons, CC BY 3.0.