Formularios
Un formulario invierte el flujo habitual de la web: en lugar de que el servidor envíe contenido al usuario, es el usuario quien envía datos al servidor. Nombres, contraseñas, mensajes, selecciones... todo eso viaja a través de formularios. Son la base de casi cualquier interactividad real: registros, búsquedas, pagos, comentarios.
La etiqueta <form>: method y action
La etiqueta <form> envuelve todos los controles del formulario y define cómo y adónde se envían los datos:
<form method="post" action="/procesar.php">
<!-- controles aquí -->
</form>
method determina cómo viajan los datos:
get: los datos se añaden a la URL como parámetros (?nombre=Ana&edad=30). Útil para búsquedas o filtros, porque la URL es compartible. No usar para datos sensibles ni para grandes volúmenes de datos.post: los datos van en el cuerpo de la petición HTTP, no en la URL. Es el método correcto para cualquier formulario que modifique datos, envíe información privada o tenga campos grandes.
action es la URL del servidor que procesará el formulario. Si se omite, los datos se envían a la misma URL de la página actual.
Nota: El formulario solo es el contenedor HTML. Para que los datos lleguen a algún sitio útil se necesita código del lado del servidor (PHP, Python, Node.js...) o un servicio externo. El HTML define la estructura y la interfaz; el servidor, la lógica.
<label>: etiquetar controles
Cada control de formulario debe tener una etiqueta visible que describa qué se espera en ese campo. La etiqueta no es solo texto: cuando está correctamente asociada al control, al hacer clic en ella el cursor salta al campo correspondiente, y los lectores de pantalla la anuncian antes del control.
La forma explícita es la más robusta: el for del <label> debe coincidir con el id del control:
<label for="nombre">Nombre completo:</label>
<input type="text" id="nombre" name="nombre">
La forma implícita envuelve el control dentro del propio <label>. Más compacta, especialmente útil para radios y checkboxes:
<label>
<input type="checkbox" name="acepta" value="si">
Acepto las condiciones
</label>
Ambas formas son válidas. El id que usa <label> debe ser único en la página.
Controles de texto
type="text" es el campo de texto libre de una sola línea. Es el tipo por defecto si se omite type:
<input type="text" id="ciudad" name="ciudad" placeholder="Por ejemplo: Madrid">
type="password" oculta los caracteres mientras se escribe. No cifra nada: la protección depende de que el formulario se envíe por HTTPS:
<input type="password" id="clave" name="clave">
<textarea> permite texto de varias líneas. El tamaño visual se controla con CSS; los atributos rows y cols sirven de respaldo pero su uso principal hoy es semántico:
<textarea id="mensaje" name="mensaje" rows="5"></textarea>
A diferencia de <input>, <textarea> es una etiqueta de apertura y cierre. El contenido inicial (si lo hay) va entre las etiquetas, no en value.
Controles de selección
Botones de opción (radio): permiten elegir exactamente una opción de un grupo. Todos los radios del mismo grupo comparten el mismo name; el value es lo que se envía cuando ese radio está seleccionado:
<label><input type="radio" name="envio" value="ordinario" checked> Envío ordinario</label>
<label><input type="radio" name="envio" value="express"> Envío express</label>
<label><input type="radio" name="envio" value="recogida"> Recogida en tienda</label>
checked marca la opción seleccionada por defecto.
Casillas de verificación (checkbox): permiten seleccionar varias opciones independientes. Cada checkbox tiene su propio name:
<label><input type="checkbox" name="newsletter" value="si" checked> Suscribirse al boletín</label>
<label><input type="checkbox" name="terminos" value="acepto"> Acepto las condiciones</label>
Un checkbox no marcado no se envía en el formulario. Si necesitas saber si un usuario desmarcó una casilla, la solución habitual es añadir un hidden con el mismo nombre y valor negativo antes del checkbox.
Menú desplegable (<select>): muestra una lista de opciones definidas con <option>:
<select id="pais" name="pais">
<option value="">Selecciona un país</option>
<option value="es">España</option>
<option value="mx" selected>México</option>
<option value="ar">Argentina</option>
</select>
selected marca la opción por defecto. El atributo value de cada <option> es lo que se envía; si se omite, se envía el texto visible.
Las opciones se pueden agrupar con <optgroup>:
<select name="ciudad">
<optgroup label="España">
<option value="mad">Madrid</option>
<option value="bcn">Barcelona</option>
</optgroup>
<optgroup label="México">
<option value="cdmx">Ciudad de México</option>
<option value="gdl">Guadalajara</option>
</optgroup>
</select>
Con el atributo multiple el menú permite seleccionar varias opciones a la vez (con Ctrl o Cmd). En la práctica, los checkboxes son más intuitivos para selección múltiple.
Tipos HTML5: email, number, date y más
HTML5 añadió nuevos valores para el atributo type de <input>. No son solo estéticos: el navegador activa el teclado adecuado en móviles y aplica validación básica automáticamente.
<!-- Valida formato de email -->
<input type="email" name="correo" placeholder="usuario@dominio.com">
<!-- Solo números; min/max/step controlan el rango -->
<input type="number" name="cantidad" min="1" max="99" step="1" value="1">
<!-- Selector de fecha nativo del navegador -->
<input type="date" name="nacimiento">
<!-- Selector de hora -->
<input type="time" name="hora">
<!-- Deslizador; útil para valores aproximados -->
<input type="range" name="volumen" min="0" max="100" step="5" value="50">
<!-- Selector de color -->
<input type="color" name="color_favorito" value="#84ba3f">
<!-- Teléfono: valida poco pero activa teclado numérico en móvil -->
<input type="tel" name="telefono" pattern="[0-9]{9}">
<!-- URL: valida que empiece por http:// o similar -->
<input type="url" name="web" placeholder="https://ejemplo.com">
<!-- Búsqueda: functionally igual que text pero con estilo de buscador -->
<input type="search" name="q" placeholder="Buscar...">
La validación que hacen estos tipos es básica (formato, rango) pero suficiente para filtrar errores involuntarios. La validación de negocio real siempre debe ocurrir también en el servidor: el cliente puede modificar el HTML.
Botones: submit, reset, <button>
type="submit" envía el formulario. type="reset" restablece todos los campos a su valor inicial. Ambos pueden implementarse con <input> o con <button>:
<!-- Con input -->
<input type="submit" value="Enviar">
<input type="reset" value="Borrar">
<!-- Con button (permite contenido HTML dentro) -->
<button type="submit"><i class="fal fa-paper-plane"></i> Enviar</button>
<button type="reset">Borrar</button>
<button type="button" id="btn-preview">Vista previa</button>
<button> es más flexible porque su contenido puede ser HTML (texto, iconos, imágenes). El atributo type="button" crea un botón que no hace nada por sí solo: su comportamiento lo define JavaScript. Sin el atributo type, un <button> dentro de un formulario actúa como submit por defecto.
Atributos de comportamiento y validación
Estos atributos controlan el comportamiento de los controles sin necesidad de JavaScript:
<!-- Campo obligatorio -->
<input type="text" name="nombre" required>
<!-- Texto de ayuda antes de escribir -->
<input type="text" name="ciudad" placeholder="Por ejemplo: Sevilla">
<!-- Validación con expresión regular -->
<input type="text" name="codigo" pattern="[A-Z]{2}[0-9]{4}" title="Dos letras y cuatro números">
<!-- Longitud máxima y mínima -->
<input type="password" name="clave" minlength="8" maxlength="64">
<!-- Campo desactivado: no se envía -->
<input type="text" name="referencia" value="REF-001" disabled>
<!-- Campo de solo lectura: sí se envía -->
<input type="text" name="usuario" value="ana.garcia" readonly>
<!-- Foco automático al cargar la página -->
<input type="text" name="busqueda" autofocus>
<!-- Autocompletado del navegador -->
<input type="email" name="correo" autocomplete="email">
El atributo name es el identificador que llega al servidor: sin él, el campo no se envía aunque tenga valor. El id es para el DOM y los <label>; el name es para el formulario. No tienen que ser iguales, aunque a menudo lo son.
<fieldset> y <legend>
<fieldset> agrupa controles relacionados y los rodea de un borde visual. <legend> añade un título a ese grupo:
<form method="post" action="/registro">
<fieldset>
<legend>Datos personales</legend>
<label for="nombre">Nombre:</label>
<input type="text" id="nombre" name="nombre" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</fieldset>
<fieldset>
<legend>Preferencias</legend>
<label><input type="checkbox" name="newsletter" value="si"> Suscribirme al boletín</label>
</fieldset>
<button type="submit">Crear cuenta</button>
</form>
Además del aspecto visual, <fieldset> tiene un rol semántico: los lectores de pantalla anuncian el <legend> antes de cada control del grupo. Es útil en formularios largos para orientar al usuario.
Un <fieldset> con el atributo disabled desactiva todos sus controles de golpe, útil cuando un grupo de campos solo aplica bajo ciertas condiciones.
Campos ocultos y subida de archivos
type="hidden" envía datos que el usuario no ve ni modifica. Sirve para pasar información de contexto al servidor (un token CSRF, un identificador de sesión, el origen del formulario):
<input type="hidden" name="csrf_token" value="a3f9b2...">
<input type="hidden" name="origen" value="pagina_contacto">
No confundas "oculto" con "seguro": cualquiera puede ver el valor en el código fuente. No guardes información sensible en campos ocultos.
type="file" permite seleccionar archivos para subir. Requiere que el formulario tenga enctype="multipart/form-data", de lo contrario solo se envía el nombre del archivo, no su contenido:
<form method="post" action="/subir" enctype="multipart/form-data">
<label for="foto">Foto de perfil:</label>
<input type="file" id="foto" name="foto" accept="image/jpeg,image/png,image/webp">
<button type="submit">Subir</button>
</form>
accept filtra los tipos de archivo en el diálogo del sistema operativo, pero no es una restricción de seguridad: el servidor debe validar el tipo real del archivo que recibe.
CSS para formularios
Los controles de formulario son notoriamente difíciles de estilizar: cada navegador tiene su propio aspecto por defecto, y algunas propiedades CSS no se aplican igual a todos los tipos de control. El punto de partida habitual es resetear los estilos base:
input,
textarea,
select {
box-sizing: border-box;
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit; /* los controles no heredan la fuente por defecto */
font-size: 1rem;
color: inherit;
background: #fff;
}
El estado de foco es crucial: indica dónde está el cursor del teclado. No elimines el outline sin proporcionar un reemplazo visible:
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #84ba3f;
box-shadow: 0 0 0 3px rgba(132, 186, 63, 0.2);
}
Los estados de validación también son estilizables. El navegador aplica :valid e :invalid automáticamente según los atributos de validación:
input:invalid {
border-color: #cc3333;
}
input:valid {
border-color: #44aa44;
}
Nota: :invalid se aplica desde que la página carga si un campo está vacío y es required, lo que muestra errores antes de que el usuario haya tocado nada. La pseudo-clase :user-invalid (soporte creciente) solo activa el estilo después de que el usuario haya interactuado con el campo.
Los campos desactivados y de solo lectura tienen también sus pseudo-clases:
input:disabled,
textarea:disabled {
background: #f5f5f5;
color: #999;
cursor: not-allowed;
}
input:read-only {
background: #fafafa;
}
Para estilizar checkboxes y radios con aspecto personalizado hay que recurrir a técnicas más complejas (ocultar el control nativo y usar ::before para dibujar uno propio). Los controles de formulario HTML tienen sus límites: para interfaces muy personalizadas se usan componentes JavaScript.
Material histórico
Los atributos size en <input> y cols en <textarea> controlaban el tamaño visual en caracteres. Son funcionales pero se considera mala práctica: mezclan presentación con estructura. La alternativa moderna es CSS (width, height).
El atributo align en formularios y controles está obsoleto desde HTML4. El layout de formularios se hace con CSS (grid o flexbox son las opciones habituales hoy).
El envío de formularios a action="mailto:correo@ejemplo.com" nunca fue una buena solución: depende del cliente de correo del usuario, expone la dirección de correo en el HTML, y el formato que llega es ilegible. Aún funciona en algunos navegadores, pero está desaconsejado. La alternativa siempre ha sido un script en el servidor.
El elemento <isindex> era un campo de búsqueda de una sola línea anterior a los formularios modernos. Obsoleto desde HTML4.
Tu proyecto
Añade una sección de contacto a proyecto.html. Con action="mailto:" el formulario abre el cliente de correo del visitante con los campos ya rellenos, sin necesitar servidor:
<h2 id="contacto">Contacto</h2>
<form class="formulario-contacto"
action="mailto:tu@email.com"
method="post"
enctype="text/plain">
<div>
<label for="nombre">Nombre</label>
<input type="text" id="nombre" name="nombre" required>
</div>
<div>
<label for="email">Correo electrónico</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="mensaje">Mensaje</label>
<textarea id="mensaje" name="mensaje" rows="5"></textarea>
</div>
<button type="submit">Enviar</button>
</form>
Nota: mailto: depende de que el visitante tenga un cliente de correo configurado en su dispositivo, algo cada vez menos común. Para un portfolio en producción lo habitual es un script de servidor o un servicio externo como Formspree o Netlify Forms. Para aprender la estructura del formulario, mailto: es perfecto.
.formulario-contacto {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 520px;
}
.formulario-contacto label {
display: block;
font-size: 0.85rem;
color: #666;
margin-bottom: 0.3rem;
}
.formulario-contacto input,
.formulario-contacto textarea {
width: 100%;
padding: 0.6rem 0.8rem;
border: 1px solid #ddd;
font-family: inherit;
font-size: 1rem;
}
.formulario-contacto input:focus,
.formulario-contacto textarea:focus {
outline: none;
border-color: #c9a96e;
}
.formulario-contacto button {
align-self: flex-start;
padding: 0.7rem 2rem;
background: #1a1a2e;
color: #fff;
border: none;
font-family: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.formulario-contacto button:hover {
background: #c9a96e;
}
Recapitulación
<form>envuelve todos los controles.method="post"envía los datos en el cuerpo de la petición;method="get"los añade a la URL.actionindica el destino.<label>debe acompañar siempre a cada control, asociado mediantefor/id(explícito) o envolviendo el control (implícito).- Los tipos básicos de
<input>:text,password,radio,checkbox,submit,reset,hidden,file. - HTML5 añadió
email,number,date,time,range,color,tel,urlysearch, con validación básica automática y teclados adaptados en móvil. <textarea>es para texto multilínea.<select>con<option>crea menús desplegables;<optgroup>agrupa opciones.<button>es más flexible que<input type="submit">: permite contenido HTML dentro. Sintype, actúa como submit dentro de un formulario.- Atributos de validación:
required,placeholder,pattern,min/max,minlength/maxlength. Elnamees imprescindible para que el campo se envíe. disableddesactiva el campo y no lo envía.readonlyimpide la edición pero sí lo envía.<fieldset>y<legend>agrupan controles relacionados con valor semántico y visual.- En CSS: resetear
font-familyeinherit, estilizar:focusde forma visible, usar:valid/:invalidcon precaución. - La validación del cliente es cómoda pero no es seguridad: siempre debe complementarse con validación en el servidor.
En la próxima lección: unidades de medida en CSS: px, rem y %, y cuándo usar cada una.