Tema
Formularios
Los formularios son la forma en que los usuarios hablan con un sitio: contacto, reservas, registro, compras, búsquedas. Para un cliente, el formulario suele ser la parte más importante de su web, porque ahí llegan los prospectos y las ventas. En esta lección los verás a fondo: todos los tipos de campo, validación nativa sin JavaScript y los detalles que hacen que un formulario sea cómodo en celular.
Concepto
Un formulario es como una hoja de solicitud en papel: tiene campos con su etiqueta ("Nombre:", "Teléfono:"), casillas para marcar y un lugar donde entregarla. En HTML:
<form>es la hoja completa.<label>es la etiqueta impresa de cada campo.<input>,<select>y<textarea>son los espacios para escribir o elegir.<button type="submit">es "entregar la hoja".
Ejemplo completo
Prueba enviarlo vacío o con datos incorrectos: el navegador valida por sí solo.
El elemento form
html
<form action="/enviar-contacto" method="post">
<!-- campos -->
<button type="submit">Enviar</button>
</form>action: URL a la que se envían los datos (un script en el servidor: PHP, Node, un servicio como Formspree, etc.). Si se omite, se envía a la misma página.method: cómo se envían:get: los datos van en la URL (/buscar?q=latte). Para búsquedas y filtros (se pueden compartir y guardar en marcadores).post: los datos van en el cuerpo de la petición. Para todo lo que crea o modifica algo: contacto, registro, pagos. Nunca envíes contraseñas conget.
Otros atributos útiles:
| Atributo | Para qué sirve |
|---|---|
enctype="multipart/form-data" | Obligatorio si el formulario sube archivos. |
novalidate | Desactiva la validación nativa (cuando validas con JS). |
autocomplete="off" | Desactiva el autocompletado en todo el formulario (rara vez conviene). |
target="_blank" | Muestra la respuesta en otra pestaña. |
El atributo name es imprescindible
Solo se envían los campos que tienen name. El servidor recibe pares name=valor. Un campo sin name es invisible para el backend.
label: siempre, siempre
html
<!-- Opción 1: for + id (la más flexible) -->
<label for="correo">Correo electrónico</label>
<input id="correo" name="correo" type="email">
<!-- Opción 2: envolver el campo -->
<label>
<input type="checkbox" name="boletin"> Quiero recibir el boletín
</label>Beneficios de un <label> bien asociado:
- Al dar clic en el texto, el foco va al campo (en checkboxes, ¡el área clicable crece muchísimo!).
- Los lectores de pantalla anuncian "Correo electrónico, campo de edición".
- Es obligatorio para la accesibilidad.
Error común
Usar placeholder en lugar de <label>. El placeholder desaparece al escribir (el usuario olvida qué pedía el campo), suele tener bajo contraste y muchos lectores de pantalla no lo leen. Úsalo solo como ejemplo del formato esperado: placeholder="55 1234 5678".
Todos los tipos de input
El atributo type cambia por completo el comportamiento del campo, el teclado en móviles y la validación.
Texto
html
<input type="text" name="nombre"> <!-- texto libre -->
<input type="email" name="correo"> <!-- valida formato de correo; teclado con @ -->
<input type="password" name="clave"> <!-- oculta los caracteres -->
<input type="tel" name="telefono"> <!-- teclado numérico de teléfono (no valida formato) -->
<input type="url" name="sitio"> <!-- valida que empiece con http:// o https:// -->
<input type="search" name="q"> <!-- búsqueda; muestra botón para limpiar -->Números y rangos
html
<input type="number" name="cantidad" min="1" max="10" step="1" value="1">
<input type="range" name="volumen" min="0" max="100" step="5" value="50">number muestra flechas para subir/bajar. Úsalo para cantidades, no para teléfonos, códigos postales o tarjetas (que no son números con los que se calcule; para esos usa text con inputmode="numeric").
Fechas y horas
html
<input type="date" name="fecha" min="2026-01-01" max="2026-12-31">
<input type="time" name="hora" min="08:00" max="21:00" step="900"> <!-- intervalos de 15 min -->
<input type="datetime-local" name="cita">
<input type="month" name="mes">
<input type="week" name="semana">El navegador muestra un calendario nativo. El valor siempre se envía en formato ISO (2026-10-18), sin importar cómo se muestre al usuario.
Selección
html
<!-- checkbox: varias opciones independientes -->
<label><input type="checkbox" name="extras" value="leche-avena"> Leche de avena</label>
<label><input type="checkbox" name="extras" value="shot-extra" checked> Shot extra</label>
<!-- radio: UNA opción de un grupo (mismo name) -->
<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>Los radios se agrupan por su name: solo uno del grupo puede estar marcado. checked marca la opción por defecto.
Otros tipos
html
<input type="color" name="color" value="#c8763a"> <!-- selector de color -->
<input type="file" name="cv" accept=".pdf,.doc,.docx"> <!-- subir archivo -->
<input type="file" name="fotos" accept="image/*" multiple> <!-- varias imágenes -->
<input type="file" accept="image/*" capture="environment"> <!-- cámara trasera en móvil -->
<input type="hidden" name="origen" value="landing-octubre"> <!-- dato oculto que se envía -->Resumen de tipos:
| Tipo | Uso típico |
|---|---|
text | Nombre, empresa, asunto |
email | Correo |
password | Contraseña |
tel | Teléfono |
url | Sitio web |
search | Buscador |
number | Cantidad, edad |
range | Deslizador (presupuesto, volumen) |
date, time, datetime-local, month, week | Reservas, citas |
checkbox | Aceptar términos, varias opciones |
radio | Una opción entre varias |
color | Elegir color |
file | Subir CV, fotos, documentos |
hidden | Datos internos (campaña, ID) |
submit, reset, button | Botones (mejor usa <button>) |
textarea
Para textos largos:
html
<label for="mensaje">Mensaje</label>
<textarea id="mensaje" name="mensaje" rows="5" minlength="20" maxlength="500"></textarea>textarea no es un elemento vacío: lleva etiqueta de cierre, y lo que escribas entre ambas es su valor inicial (¡incluidos espacios y saltos de línea!).
select, option y optgroup
html
<label for="servicio">Servicio</label>
<select id="servicio" name="servicio" required>
<!-- Opción vacía: obliga a elegir conscientemente -->
<option value="">Elige una opción</option>
<optgroup label="Sitios web">
<option value="landing">Landing page</option>
<option value="corporativo" selected>Sitio corporativo</option>
</optgroup>
<optgroup label="Otros">
<option value="tienda">Tienda en línea</option>
<option value="mantenimiento" disabled>Mantenimiento (agotado)</option>
</optgroup>
</select>
<!-- Selección múltiple (Ctrl+clic). Poco amigable: prefiere checkboxes -->
<select name="dias" multiple size="4">
<option>Lunes</option>
<option>Martes</option>
</select>valuees lo que se envía; el texto de la opción es lo que ve el usuario.- Con más de ~7 opciones,
<select>es mejor que radios; con pocas, los radios son más rápidos de usar.
datalist: sugerencias
<datalist> ofrece sugerencias mientras escribes, pero permite escribir cualquier valor:
html
<label for="ciudad">Ciudad</label>
<input id="ciudad" name="ciudad" list="ciudades">
<datalist id="ciudades">
<option value="Ciudad de México"></option>
<option value="Guadalajara"></option>
<option value="Monterrey"></option>
</datalist>fieldset y legend
Agrupan campos relacionados. Son obligatorios en la práctica para grupos de radios o checkboxes, porque el <legend> es la "pregunta" que el lector de pantalla anuncia:
html
<fieldset>
<legend>¿Cómo prefieres que te contactemos?</legend>
<label><input type="radio" name="contacto" value="correo"> Correo</label>
<label><input type="radio" name="contacto" value="telefono"> Teléfono</label>
<label><input type="radio" name="contacto" value="whatsapp"> WhatsApp</label>
</fieldset>
<!-- disabled en fieldset desactiva todos sus campos -->
<fieldset disabled>
<legend>Datos de facturación (próximamente)</legend>
<input name="rfc">
</fieldset>Botones
html
<button type="submit">Enviar</button> <!-- envía el formulario -->
<button type="reset">Limpiar</button> <!-- restablece los valores (úsalo poco) -->
<button type="button">Ver más</button> <!-- no hace nada por sí solo: para JavaScript -->Error común
Un <button> dentro de un <form> sin type es submit por defecto. Si pones un botón para "mostrar contraseña" sin type="button", ¡enviará el formulario! Escribe siempre el type.
Prefiere <button> sobre <input type="submit">: puede contener HTML (iconos, <span>) y es más fácil de estilizar.
Validación nativa
El navegador puede validar sin una sola línea de JavaScript. Si algo falla, no envía el formulario y muestra un mensaje junto al campo.
html
<!-- Obligatorio -->
<input name="nombre" required>
<!-- Longitud de texto -->
<input name="usuario" minlength="4" maxlength="20">
<!-- Rango numérico o de fechas -->
<input type="number" name="personas" min="1" max="12">
<input type="date" name="fecha" min="2026-10-18">
<!-- Intervalos: solo múltiplos de 0.5 -->
<input type="number" name="kilos" step="0.5">
<!-- El propio type valida el formato -->
<input type="email" name="correo">
<input type="url" name="sitio">
<!-- Expresión regular: exactamente 10 dígitos -->
<input type="tel" name="telefono" pattern="[0-9]{10}"
title="Escribe 10 dígitos, sin espacios">
<!-- Código postal mexicano: 5 dígitos -->
<input name="cp" pattern="\d{5}" inputmode="numeric">Atributos de validación:
| Atributo | Valida |
|---|---|
required | Que no esté vacío (en checkbox: que esté marcado; en radios: que haya uno elegido). |
minlength / maxlength | Longitud mínima/máxima del texto. |
min / max | Valor mínimo/máximo (números, fechas, horas). |
step | Incrementos permitidos. |
pattern | Que el texto coincida con una expresión regular (completa). |
type | Formato de email, url, number, etc. |
El title en un campo con pattern se agrega al mensaje de error del navegador: úsalo para explicar el formato.
Estilos de validación con CSS
css
/* :invalid se aplica desde el inicio (un campo required vacío ya es inválido) */
input:invalid { border-color: crimson; }
/* :user-invalid solo después de que el usuario interactuó con el campo: mejor experiencia */
input:user-invalid {
border-color: crimson;
background: #fef2f2;
}
input:user-valid { border-color: seagreen; }
/* Otros estados útiles */
input:required { } /* campos obligatorios */
input:focus-visible { } /* foco con teclado */
input:disabled { }
input:placeholder-shown { } /* mientras se ve el placeholder */Buena práctica
Usa :user-invalid en lugar de :invalid para no pintar de rojo el formulario completo antes de que el usuario escriba algo. Ya tiene soporte en todos los navegadores modernos.
Mensajes personalizados (un poquito de JS)
Si quieres cambiar el texto del mensaje nativo:
js
const tel = document.querySelector('#telefono');
tel.addEventListener('input', () => {
if (tel.validity.patternMismatch) {
tel.setCustomValidity('El teléfono debe tener 10 dígitos, sin espacios.');
} else {
tel.setCustomValidity(''); // vacío = válido
}
});Esto lo dominarás en el curso de JavaScript.
La validación del navegador no es seguridad
Cualquiera puede saltarse la validación HTML (desde las herramientas de desarrollador o enviando datos directamente al servidor). Sirve para la experiencia del usuario. El servidor siempre debe volver a validar todo.
autocomplete: formularios que se llenan solos
El atributo autocomplete le dice al navegador qué dato pide cada campo, para que lo rellene con la información guardada del usuario. Es una mejora enorme en móviles y aumenta las conversiones.
html
<input name="nombre" autocomplete="name">
<input name="correo" type="email" autocomplete="email">
<input name="telefono" type="tel" autocomplete="tel">
<input name="empresa" autocomplete="organization">
<input name="direccion" autocomplete="street-address">
<input name="cp" autocomplete="postal-code">
<input name="ciudad" autocomplete="address-level2">
<input name="pais" autocomplete="country-name">
<!-- Inicio de sesión y registro -->
<input name="usuario" autocomplete="username">
<input name="clave" type="password" autocomplete="current-password">
<input name="nueva" type="password" autocomplete="new-password">
<!-- Códigos de verificación por SMS -->
<input name="codigo" autocomplete="one-time-code" inputmode="numeric">
<!-- Tarjetas -->
<input name="tarjeta" autocomplete="cc-number" inputmode="numeric">
<input name="vence" autocomplete="cc-exp">inputmode: el teclado correcto en móvil
inputmode cambia el teclado virtual sin cambiar la validación:
| Valor | Teclado |
|---|---|
numeric | Solo números (CP, PIN, tarjeta) |
decimal | Números con punto decimal (precios) |
tel | Teclado telefónico |
email | Con @ y punto |
url | Con / y .com |
search | Con botón "Buscar" |
none | Sin teclado (para selectores propios) |
html
<!-- Código postal: texto (conserva ceros iniciales) con teclado numérico -->
<input name="cp" inputmode="numeric" pattern="\d{5}" autocomplete="postal-code">Otros atributos útiles
html
<input name="folio" value="A-1023" readonly> <!-- se ve y se envía, pero no se edita -->
<input name="cupon" disabled> <!-- ni se edita ni se envía -->
<input name="nombre" autofocus> <!-- recibe el foco al cargar (úsalo con cuidado) -->
<input name="q" spellcheck="false"> <!-- sin corrector ortográfico -->
<input name="nombre" autocapitalize="words"> <!-- mayúscula inicial en móvil -->
<input name="correo" enterkeyhint="next"> <!-- tecla Enter muestra "Siguiente" -->Y dos elementos de salida:
html
<!-- output: muestra un resultado calculado -->
<output name="total" for="cantidad">$0</output>
<!-- progress y meter -->
<progress value="60" max="100">60%</progress>
<meter value="0.8" min="0" max="1" low="0.3" high="0.7">80%</meter>Casos de uso reales
- Formulario de contacto de una agencia: nombre, correo, teléfono, servicio (select), presupuesto (radios), mensaje y aceptación de privacidad.
- Reserva de restaurante:
datetime-localconmin, número de personas, teléfono conpattern. - Bolsa de trabajo:
input type="file"conaccept=".pdf"yenctype="multipart/form-data". - Buscador del sitio:
method="get"coninput type="search"para que la URL se pueda compartir. - Envío sin backend: servicios como Formspree, Netlify Forms o Web3Forms reciben el formulario y te mandan un correo; solo cambias el
action.
Ejercicios
- Crea un formulario de registro con: nombre (obligatorio, mínimo 3 caracteres), correo, contraseña (mínimo 8), fecha de nacimiento, género (radios dentro de fieldset) y aceptación de términos (obligatorio). Usa
autocompletecorrecto. - Crea un buscador que envíe a
/buscarconget. - Crea un campo de código postal de 5 dígitos con teclado numérico y mensaje de ayuda.
Ver solución
html
<!-- Ejercicio 1 -->
<form action="/registro" method="post">
<label for="r-nombre">Nombre</label>
<input id="r-nombre" name="nombre" required minlength="3" autocomplete="name">
<label for="r-correo">Correo</label>
<input id="r-correo" name="correo" type="email" required autocomplete="email">
<label for="r-clave">Contraseña</label>
<input id="r-clave" name="clave" type="password" required minlength="8" autocomplete="new-password">
<label for="r-nac">Fecha de nacimiento</label>
<input id="r-nac" name="nacimiento" type="date" max="2026-10-18" autocomplete="bday">
<fieldset>
<legend>Género</legend>
<label><input type="radio" name="genero" value="mujer"> Mujer</label>
<label><input type="radio" name="genero" value="hombre"> Hombre</label>
<label><input type="radio" name="genero" value="otro"> Otro</label>
<label><input type="radio" name="genero" value="ninguno"> Prefiero no decirlo</label>
</fieldset>
<label>
<input type="checkbox" name="terminos" required>
Acepto los <a href="/terminos">términos y condiciones</a>
</label>
<button type="submit">Crear cuenta</button>
</form>
<!-- Ejercicio 2 -->
<form action="/buscar" method="get" role="search">
<label for="q">Buscar en el sitio</label>
<input id="q" name="q" type="search" required>
<button type="submit">Buscar</button>
</form>
<!-- Ejercicio 3 -->
<label for="cp">Código postal</label>
<input id="cp" name="cp" inputmode="numeric" pattern="\d{5}" maxlength="5"
autocomplete="postal-code" aria-describedby="cp-ayuda"
title="5 dígitos">
<p id="cp-ayuda">Ejemplo: 04000</p>Resumen
<form>conactionymethod(getpara búsquedas,postpara enviar datos).- Cada campo necesita
name(para enviarse) y un<label>asociado (para ser accesible). - Elige el
typecorrecto: cambia el teclado, el control y la validación. - Agrupa radios y checkboxes en
<fieldset>con<legend>. - Escribe siempre el
typede los<button>. - Valida con
required,pattern,min/max,minlength/maxlength; estiliza con:user-invalid. autocompleteeinputmodehacen que llenar formularios en celular sea rápido.- La validación del navegador mejora la experiencia, pero el servidor siempre debe validar.