Etiqueta form en HTML: envío, métodos y validación
La etiqueta <form> agrupa controles para enviar datos del usuario a un destino definido. Úsala para una búsqueda, una inscripción o una solicitud: reúne los campos y establece cómo se entrega la información. HTML prepara el envío; para recibirlo, comprobarlo y guardarlo necesitas implementar el destino.
Un ejemplo mínimo de form
<form action="/buscar.html" method="get">
<label for="q">Buscar</label>
<input id="q" name="q" type="search">
<button type="submit">Buscar</button>
</form>
Verás un campo con su etiqueta y un botón. Escribe html y activa «Buscar»: el navegador navega a /buscar.html?q=html en el mismo sitio. La clave q procede de name, y html es el valor escrito. También puedes iniciar el envío con Enter desde este campo.
El ejemplo no instala un buscador. /buscar.html es un destino propuesto que tendrás que implementar; si no existe, podrás obtener una respuesta de error aunque la URL esté bien formada. Para probar el fragmento, colócalo dentro del <body> de un documento HTML con <!doctype html>, idioma español, codificación UTF-8 y viewport adaptable.
Sintaxis y contexto válido
Escribe apertura y cierre: <form>…</form>. El elemento puede aparecer donde se admite contenido de flujo, por ejemplo dentro de una sección. Puede contener párrafos, agrupaciones y controles, pero no otro <form> descendiente, aunque haya contenedores intermedios. Tampoco lo introduzcas dentro de un <p>: ese elemento no admite un formulario. Son restricciones del modelo de contenido de form.
Un control puede quedar fuera y conservar su asociación mediante el atributo form, cuyo valor es el id del formulario:
<form id="consulta" action="/buscar.html" method="get">
<button type="submit">Buscar</button>
</form>
<label for="consulta-texto">Término de búsqueda</label>
<input id="consulta-texto" form="consulta" name="q" type="search">
Ese campo participa en el envío aunque no sea hijo del formulario. El atributo se escribe en cada control que necesite la asociación; colocarlo en un contenedor genérico no lo transmite a sus hijos. Consulta las reglas de asociación de controles.
Si sustituyes el formulario por un <div>, los controles siguen visibles, pero pierden ese formulario asociado y su envío nativo. Cambiar su margen con CSS solo cambia la presentación.
Qué datos se envían: name, id y value
Estos atributos cumplen funciones distintas:
| Dato del control | Función |
|---|---|
name="email" | Define la clave que recibirá el destino. |
id="correo" | Identifica el elemento para una etiqueta, CSS o JavaScript; no reemplaza a name. |
value | Define un valor inicial o el valor asociado al control, según su tipo. En un campo de texto se envía el valor actual, que puede haber cambiado. |
No se envía automáticamente todo lo que aparece en pantalla. En los controles comunes, un campo sin name o con un nombre vacío queda fuera. Los controles con disabled también quedan fuera, incluso si conservan un valor visible. Entre casillas y botones de opción, participan los marcados; una casilla sin marcar no envía una clave con el valor false. Para una casilla marcada, value="si" envía si; sin value, su valor predeterminado es on.
Del conjunto de botones de envío, solo participa el que inició ese envío, si tiene nombre. Por ejemplo, un botón con name="accion" value="inscribir" añade accion=inscribir cuando se activa. Los controles con el mismo nombre pueden producir varias entradas: no supongas que cada clave aparece una sola vez. Estas reglas forman parte de la construcción de la lista de datos.
Elegir GET o POST y validar los datos
Usa method="get" para consultas que no solicitan cambios, como buscar o filtrar. Los pares nombre-valor aparecen codificados en la URL, que puedes copiar o guardar. No uses GET para contraseñas: los datos pueden quedar expuestos en el historial y en registros de URLs.
Usa method="post" cuando solicites un efecto, como crear una inscripción. Los datos viajan en el cuerpo de la petición en lugar de la consulta de la URL. POST no cifra los datos por sí mismo: utiliza HTTPS tanto para la página como para el destino. El cifrado del transporte procede de TLS; tampoco garantiza que el servidor almacene los datos de forma adecuada. Consulta las semánticas de GET y POST y la función de TLS.
required y type="email" permiten al navegador comprobar campos antes de enviarlos. Un correo con formato admitido no demuestra que la dirección exista ni pertenezca a quien la escribió. La validación del cliente puede eludirse: el servidor debe comprobar de nuevo formato, valores permitidos y condiciones de la operación. El estándar explica este límite de seguridad de la validación.
Caso práctico: inscripción local sin servidor
Guarda el siguiente documento completo como inscripcion.html, con codificación UTF-8, y ábrelo en un navegador actual. Usa datos ficticios. No requiere recursos externos: el script intercepta submit, muestra FormData y evita la petición. No envía ni guarda una inscripción, y al recargar se pierde la vista de los datos.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prueba local de inscripción</title>
<style>
body { font-family: system-ui, sans-serif; margin: 1rem; }
#form-inscripcion input[type="email"] {
box-sizing: border-box;
max-width: 100%;
}
#form-datos { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<h1 id="form-titulo">Inscripción de prueba al taller</h1>
<p>Los dos campos son obligatorios. No se enviará información.</p>
<form id="form-inscripcion" method="post"
aria-labelledby="form-titulo">
<fieldset id="form-campos" disabled>
<legend>Datos de prueba</legend>
<p>
<label for="form-email">Correo electrónico (obligatorio)</label>
<input id="form-email" name="email" type="email"
autocomplete="email" required
aria-describedby="form-email-ayuda form-email-error">
<small id="form-email-ayuda">Prueba con ana@example.test.</small>
<span id="form-email-error"></span>
</p>
<p>
<input id="form-consentimiento" name="consentimiento"
type="checkbox" value="si" required
aria-describedby="form-consentimiento-error">
<label for="form-consentimiento">
Acepto participar en esta inscripción de prueba (obligatorio)
</label>
<span id="form-consentimiento-error"></span>
</p>
<button type="submit" name="accion" value="inscribir">
Ver datos de prueba
</button>
</fieldset>
</form>
<p id="form-estado" role="status">Ningún dato se ha enviado ni guardado.</p>
<pre id="form-datos" aria-label="Datos del formulario"></pre>
<noscript>
<p>Esta vista local necesita JavaScript. Los controles están
deshabilitados; puedes estudiar el código y sus valores esperados.</p>
</noscript>
<script>
(() => {
const formulario = document.getElementById("form-inscripcion");
const campos = document.getElementById("form-campos");
const estado = document.getElementById("form-estado");
const salida = document.getElementById("form-datos");
if (!("FormData" in window)) {
estado.textContent = "Este navegador no dispone de FormData.";
return;
}
formulario.addEventListener("invalid", (evento) => {
const control = evento.target;
const error = document.getElementById(control.id + "-error");
if (error) {
error.textContent = control.validationMessage;
control.setAttribute("aria-invalid", "true");
}
}, true);
formulario.addEventListener("input", (evento) => {
const control = evento.target;
const error = document.getElementById(control.id + "-error");
if (error) {
error.textContent = "";
control.removeAttribute("aria-invalid");
}
salida.textContent = "";
estado.textContent = "Valores modificados; vuelve a comprobarlos.";
});
formulario.addEventListener("submit", (evento) => {
evento.preventDefault();
const datos = new FormData(formulario, evento.submitter);
salida.textContent = Array.from(datos.entries())
.map(([nombre, valor]) => nombre + " = " + valor)
.join("\n");
estado.textContent = "Datos mostrados; no se han enviado ni guardado.";
});
campos.disabled = false;
})();
</script>
</body>
</html>
Prueba el comportamiento en este orden:
- Activa el botón con los campos vacíos. El navegador impide el envío y lleva el foco al primer campo inválido; aparecen mensajes asociados a los controles.
- Escribe
ana@example.test, deja la casilla sin marcar y vuelve a intentarlo. La aceptación obligatoria sigue pendiente. - Marca la casilla con Espacio y activa el botón con Enter. Verás tres líneas:
email = ana@example.test,consentimiento = siyaccion = inscribir. El foco permanece en el botón y los valores se conservan. - Modifica un campo: la vista anterior se borra para no confundirla con los valores actuales. Deshabilita JavaScript y recarga: verás la explicación de
<noscript>y los controles permanecerán deshabilitados.
FormData recoge entradas; no inicia una petición. Su segundo argumento incluye el botón que originó el envío, algo que new FormData(formulario) por sí solo no hace. Es una API de JavaScript definida en el estándar XMLHttpRequest.
Para una inscripción real, primero implementa un destino que acepte POST, por ejemplo /inscripciones, con las claves email, consentimiento=si y accion=inscribir. Esa ruta es una propuesta, no un servicio de este sitio. Después añade action="/inscripciones", retira el script de demostración y el disabled inicial, y adapta los mensajes al resultado del servidor. La respuesta debe confirmar solo lo que se haya procesado. El envío HTML nativo funciona también sin JavaScript; la vista local se bloquea sin él para evitar un envío accidental a la página actual.
Atributos propios del formulario
| Atributo | Uso y valores |
|---|---|
action | URL del destino. Si lo omites, se utiliza la URL del documento actual; eso no crea un procesador de datos. |
method | get es el predeterminado; post envía en el cuerpo. dialog, dentro de un <dialog>, cierra el diálogo sin enviar una petición HTTP. |
enctype | Codificación del envío POST. El valor predeterminado es application/x-www-form-urlencoded; usa multipart/form-data para enviar archivos. text/plain es otra opción, poco apropiada para un contrato de datos estructurado. |
autocomplete | on, predeterminado, u off como preferencia general de autocompletado. Los controles pueden concretarla, como con autocomplete="email"; el navegador puede aplicar sus propias políticas. |
novalidate | Booleano: su presencia omite la validación interactiva del envío. novalidate="false" también la omite; para quitar ese efecto, elimina el atributo. |
target | Dónde se muestra la respuesta: _self, predeterminado, o destinos como _blank, que abre otro contexto. No decide dónde se guardan los datos. |
name | Nombre del formulario para APIs como document.forms. No sustituye los nombres de los campos ni el id usado por form="…". Si se declara, debe ser no vacío y único entre los formularios de esa colección. |
rel | Relaciones y anotaciones del enlace creado al enviar, por ejemplo search, noopener o noreferrer, según el caso. _blank ya aplica el comportamiento de noopener de forma implícita; informa si abres otra pestaña. |
accept-charset | Si lo declaras, el valor admitido actualmente es UTF-8. No confundas codificación de caracteres con enctype. |
Para subir un archivo mediante el envío nativo combina method="post", enctype="multipart/form-data" y un <input type="file" name="archivo">. Aún necesitas un destino que reciba ese formato y compruebe el archivo. Un botón de envío puede sustituir ajustes mediante formaction, formmethod, formenctype, formtarget o formnovalidate; revisa el botón activado si el destino cambia. Consulta los atributos de envío y MDN sobre form.
id, class, lang y aria-labelledby son atributos globales. margin es CSS; FormData, .disabled y los manejadores de eventos pertenecen a JavaScript. required y readonly se aplican a controles compatibles, no al <form>.
Accesibilidad y errores frecuentes
Cada control necesita un nombre comprensible mediante <label> y las instrucciones deben verse antes de completar los datos. El título identifica la inscripción mediante aria-labelledby; un formulario con nombre accesible puede servir como región de navegación para tecnologías de asistencia. No hace falta añadir role="form".
La demo utiliza aria-describedby para asociar ayuda y errores, y aria-invalid cuando detecta un fallo. No cancela los eventos de validación, así que conserva la gestión nativa del foco. Al mostrar un resultado no mueve el foco ni borra los campos; role="status" anuncia la actualización según el navegador y la tecnología de asistencia. Para errores recibidos del servidor, conserva lo escrito, muestra la causa junto al campo y permite llegar al primer error. No dependas solo del color. La guía de notificaciones de WAI explica estas asociaciones.
No confundas disabled con readonly: un campo deshabilitado queda fuera de los datos y normalmente del recorrido de teclado. Un campo de texto de solo lectura puede recibir foco y participar en el envío si tiene name; no se puede editar desde la interfaz y queda fuera de la validación de restricciones. readonly no sirve para casillas ni botones. Tampoco protege un valor frente a modificaciones deliberadas. Consulta la definición de readonly.
Otros fallos habituales son anidar formularios, usar un botón auxiliar sin type="button" y esperar que un archivo HTML estático procese POST. No sustituyas una confirmación del servidor por un mensaje local de «inscripción guardada».
Preguntas y etiquetas relacionadas
¿Por qué un campo no se envía? Comprueba su name, que esté habilitado y que pertenezca al formulario correcto. Revisa si está deshabilitado por un <fieldset>, si la casilla o el radio está marcado y si se trata de un botón distinto del que inició el envío. En JavaScript, inspecciona las entradas de FormData; revisar solo el id no basta.
¿POST hace seguros los datos? No: cambia dónde viajan, pero no aporta cifrado. Necesitas HTTPS, validación del servidor y protección adecuada para la operación y el almacenamiento.
¿Puedo hacer un formulario HTML sin servidor? Puedes crear controles, validar datos y mostrarlos localmente como en esta demo. Una búsqueda con resultados calculados o una inscripción persistente necesita implementar esa función, ya sea localmente o en un destino real; <form> por sí solo no la crea.
<input> captura un valor y <label> nombra el control. <button> inicia una acción o el envío según su tipo. <fieldset> agrupa controles relacionados con una leyenda; puede estar dentro de un formulario y no sustituye su destino ni su método.
Compatibilidad y fuentes
<form> y el envío nativo tienen soporte amplio en los navegadores actuales. La compatibilidad de una API adicional debe revisarse por separado: a 3 de octubre de 2026, los datos de compatibilidad de MDN para FormData sitúan el argumento submitter en Chrome y Edge 112, Firefox 111 y Safari 16.4. En versiones anteriores, esta demo puede no incluir correctamente el dato del botón, aunque el formulario HTML funcione.
La definición y las restricciones proceden del HTML Living Standard; el algoritmo de envío explica la navegación y los datos. La referencia de FormData complementa las instrucciones de prueba. El soporte del elemento no garantiza una recepción, una inscripción ni un anuncio idéntico en todos los lectores de pantalla.