Formularios & Validación

Los formularios son el puente entre el usuario y tu aplicación: son cómo recolectas datos, registras usuarios, procesas búsquedas y permites interacción. Un buen formulario no solo funciona correctamente, sino que es fácil de usar, accesible y está bien validado. HTML5 trae herramientas poderosas de validación nativa que te ahorran escribir JavaScript para los casos más comunes.

Estructura de un formulario

Todo formulario empieza con la etiqueta <form>, que define el contenedor y los atributos de comportamiento: action indica a dónde se envían los datos, y method define cómo se envían. Dentro del form van los controles (inputs, selects, textareas) y sus etiquetas asociadas. El botón de envío (<button type="submit">) dispara el formulario.

El atributo method tiene dos valores principales: GET envía los datos en la URL (visible en la barra de direcciones, ideal para búsquedas y filtros) y POST los envía en el cuerpo del request (invisible, ideal para datos sensibles, formularios de login, cargas de archivos). Si omitis method, el default es GET, lo que es peligroso para datos privados.

HTML
<!-- Estructura basica de un formulario -->
<form action="/procesar" method="POST">
    <!-- Controles del formulario aqui -->
    <button type="submit">Enviar</button>
</form>

<!-- Formulario de busqueda (usa GET) -->
<form action="/buscar" method="GET">
    <input type="search" name="q" placeholder="Buscar...">
    <button type="submit">Buscar</button>
</form>

No uses GET para datos sensibles

Con method="GET", los datos del formulario aparecen en la URL como parámetros de query. Esto significa que quedan visibles en la barra de direcciones, en el historial del navegador, en los logs del servidor y se pueden compartir accidentalmente al copiar el enlace. Para passwords, datos personales o cualquier información sensible, usa siempre method="POST".

<label>, <fieldset> y accesibilidad

La etiqueta <label> es probablemente la más importante para la accesibilidad de formularios. Asocia un texto descriptivo con un control específico mediante el atributo for (que debe coincidir con el id del input). Cuando un usuario hace click en el label, el foco se mueve automáticamente al input asociado. Esto no solo es útil para usuarios de mouse, sino que es crítico para lectores de pantalla que leen el texto del label como la descripción del campo.

<fieldset> agrupa controles relacionados y <legend> le da un título al grupo. Es especialmente útil para formularios largos con múltiples secciones (datos personales, dirección, preferencias), para grupos de radio buttons, y para checkbox groups. Los lectores de pantalla anuncian el legend como el título del grupo, lo que da contexto a cada opción.

HTML
<!-- Label con for/id (forma correcta) -->
<label for="nombre">Nombre completo</label>
<input type="text" id="nombre" name="nombre">

<!-- Label envolviendo el input (alternativa valida) -->
<label>
    Email
    <input type="email" name="email">
</label>

<!-- Fieldset con legend para agrupar -->
<fieldset>
    <legend>Datos personales</legend>

    <label for="nombre">Nombre</label>
    <input type="text" id="nombre" name="nombre" required>

    <label for="apellido">Apellido</label>
    <input type="text" id="apellido" name="apellido" required>
</fieldset>

<!-- Radio buttons con fieldset -->
<fieldset>
    <legend>Nivel de experiencia</legend>
    <label><input type="radio" name="nivel" value="principiante"> Principiante</label>
    <label><input type="radio" name="nivel" value="intermedio"> Intermedio</label>
    <label><input type="radio" name="nivel" value="avanzado"> Avanzado</label>
</fieldset>

Nunca uses placeholder como label

El atributo placeholder desaparece cuando el usuario empieza a escribir, dejando al usuario sin referencia de qué se esperaba en ese campo. Los lectores de pantalla no lo anuncian de forma confiable. Siempre usa <label> con for. El placeholder es un complemento opcional para dar un ejemplo del formato esperado, nunca un reemplazo del label.

Tipos de input

HTML5 introdujo una gran variedad de tipos de input que van mucho más allá de text y password. Cada tipo activa controles nativos del navegador adaptados al tipo de dato: un teclado numérico en móvil para tel y number, un selector de fecha para date, un color picker para color, y validación automática para email y url. Usar el tipo correcto mejora la experiencia del usuario, especialmente en dispositivos móviles donde el teclado cambiado puede hacer una gran diferencia.

Tipo Uso Validación automática
text Texto genérico (default) Ninguna
email Direcciones de email Requiere formato de email
password Contraseñas (oculto) Ninguna
number Números (spinner) Solo números
tel Teléfonos Ninguna (teclado numérico en móvil)
url URLs / enlaces Requiere formato de URL
date Fecha (selector nativo) Formato de fecha válido
time Hora (selector nativo) Formato de hora válido
datetime-local Fecha y hora juntos Formato combinado válido
month Selector de mes y año Formato de mes válido
week Selector de semana Formato de semana válido
range Slider (valor entre min y max) Número dentro del rango
color Selector de color Formato hexadecimal (#RRGGBB)
search Campos de búsqueda (con X para limpiar) Ninguna
file Subida de archivos Ninguna
checkbox Selección múltiple (toggle) Ninguna
radio Selección única (grupo) Ninguna
hidden Datos invisibles para el usuario Ninguna
HTML
<!-- Ejemplos de tipos de input -->

<!-- Email con validacion nativa -->
<label for="email">Email</label>
<input type="email" id="email" name="email"
       placeholder="[email protected]" required>

<!-- Telefono (teclado numerico en mobile) -->
<label for="telefono">Teléfono</label>
<input type="tel" id="telefono" name="telefono"
       placeholder="+54 11 1234-5678">

<!-- Fecha de nacimiento -->
<label for="nacimiento">Fecha de nacimiento</label>
<input type="date" id="nacimiento" name="nacimiento"
       min="1900-01-01" max="2026-06-10">

<!-- Slider de volumen -->
<label for="volumen">Volumen: <output id="vol-output">50</output></label>
<input type="range" id="volumen" name="volumen"
       min="0" max="100" value="50">

<!-- Selector de color -->
<label for="color-fav">Color favorito</label>
<input type="color" id="color-fav" name="color" value="#58a6ff">

<!-- Subir archivo -->
<label for="avatar">Avatar</label>
<input type="file" id="avatar" name="avatar"
       accept="image/png, image/jpeg, image/webp">

<!-- Checkbox -->
<label>
    <input type="checkbox" name="newsletter" value="si">
    Quiero recibir el newsletter
</label>

El atributo accept en file inputs

El atributo accept en <input type="file"> filtra qué tipos de archivos puede seleccionar el usuario. Acepta MIME types (image/png), extensiones (.pdf), o comodines (image/*). Es un filtro del lado del cliente (se puede saltar), así que siempre valida también en el servidor.

Visual
text
email
search
number
range
color
date
checkbox
radio

<textarea> y <select>

<textarea> es para textos largos de varias líneas (mensajes, comentarios, descripciones). A diferencia de <input>, tiene una etiqueta de apertura y cierre, y el contenido entre ambas es el valor inicial. Los atributos rows y cols definen el tamaño inicial, pero lo más común es usar CSS para controlarlo. Con maxlength limitas la cantidad de caracteres, y con minlength estableces un mínimo (validación al enviar).

<select> crea un menú desplegable con opciones predefinidas. Cada opción va dentro de <option>, y podés agruparlas con <optgroup>. El atributo multiple permite selección múltiple (con Ctrl/Cmd). El <datalist> es una alternativa interesante: en lugar de un dropdown cerrado, ofrece sugerencias mientras el usuario escribe, como un autocompletado nativo.

HTML
<!-- Textarea para comentarios -->
<label for="mensaje">Mensaje</label>
<textarea id="mensaje" name="mensaje" rows="5"
          maxlength="500" placeholder="Escribi tu mensaje...">
</textarea>
<small>Máximo 500 caracteres</small>

<!-- Select basico -->
<label for="pais">País</label>
<select id="pais" name="pais" required>
    <option value="">Seleccioná un país</option>
    <optgroup label="América">
        <option value="ar">Argentina</option>
        <option value="mx">México</option>
        <option value="co">Colombia</option>
        <option value="cl">Chile</option>
    </optgroup>
    <optgroup label="Europa">
        <option value="es">España</option>
        <option value="it">Italia</option>
    </optgroup>
</select>

<!-- Select multiple -->
<label for="skills">Habilidades (Ctrl + click para varias)</label>
<select id="skills" name="skills" multiple size="5">
    <option value="html">HTML</option>
    <option value="css">CSS</option>
    <option value="js">JavaScript</option>
    <option value="react">React</option>
    <option value="node">Node.js</option>
</select>

<!-- Datalist: autocompletado nativo -->
<label for="framework">Framework favorito</label>
<input type="text" id="framework" name="framework"
       list="frameworks-list">
<datalist id="frameworks-list">
    <option value="React">
    <option value="Vue">
    <option value="Angular">
    <option value="Svelte">
    <option value="Astro">
</datalist>

datalist vs select

La diferencia clave es la flexibilidad: <select> obliga al usuario a elegir una de las opciones predefinidas. <datalist> muestra sugerencias pero permite escribir un valor custom. Es ideal para campos donde querés guiar al usuario sin restringirlo, como "ciudad" o "cargo". La combinación input + datalist crea un campo con autocompletado nativo sin JavaScript.

Visual
select
textarea
datalist
fieldset
Nivel

Validación nativa de HTML5

HTML5 trae validación incorporada que funciona sin JavaScript. El navegador verifica los campos antes de enviar el formulario y muestra mensajes de error contextuales. Esto cubre los casos más comunes y es perfectamente suficiente para muchos formularios. La validación se activa al hacer click en el botón submit (o al llamar a form.reportValidity()), no mientras el usuario escribe (a menos que uses CSS :invalid para feedback en tiempo real).

Experimentá con la validación nativa en vivo. Elegí un preset de formulario, activá o desactivá atributos de validación con los checkboxes del panel derecho, y escribi en los campos para ver cómo reaccionan. El panel inferior muestra el estado de la Validity API de cada campo en tiempo real. Probá el toggle novalidate para ver la diferencia entre validación nativa y sin validación.

Playground — Validación de Formularios

Atributos de validación

Los atributos de validación son booleanos (se activan con solo estar presentes) o toman un valor específico. Combinarlos te permite crear reglas bastante sofisticadas sin una línea de JavaScript. required es el más básico: el campo no puede estar vacío. minlength y maxlength controlan la longitud del texto. min, max y step funcionan con números y fechas. pattern acepta una regex para validación personalizada de texto.

HTML
<!-- required: campo obligatorio -->
<label for="nombre">Nombre *</label>
<input type="text" id="nombre" name="nombre" required>

<!-- minlength / maxlength -->
<label for="usuario">Usuario</label>
<input type="text" id="usuario" name="usuario"
       minlength="3" maxlength="20"
       placeholder="3 a 20 caracteres">

<!-- min / max / step (numeros) -->
<label for="edad">Edad</label>
<input type="number" id="edad" name="edad"
       min="18" max="120" step="1">

<!-- min / max (fechas) -->
<label for="evento">Fecha del evento</label>
<input type="date" id="evento" name="evento"
       min="2026-06-10">

<!-- pattern: regex personalizada -->
<label for="codigo-postal">Código Postal</label>
<input type="text" id="codigo-postal" name="codigo-postal"
       pattern="[0-9]{4,5}"
       placeholder="Ej: 1234 o B7400">

<!-- pattern para password fuerte -->
<label for="clave">Contraseña</label>
<input type="password" id="clave" name="clave"
       minlength="8"
       pattern="(?=.*[A-Z])(?=.*\d).{8,}"
       title="Mínimo 8 caracteres, una mayúscula y un número">

Pseudo-clases CSS para validación

CSS tiene pseudo-clases que se activan según el estado de validación del input, lo que te permite dar feedback visual en tiempo real sin JavaScript. :valid y :invalid se aplican según si el campo cumple las reglas HTML. :required y :optional identifican campos obligatorios. :focus-visible muestra el foco solo con teclado. :placeholder-shown detecta si el placeholder está visible (campo vacío). Combinar estas pseudo-clases te da control total sobre el aspecto visual de cada estado del input.

CSS
/* Feedback visual con pseudo-clases */

/* Solo mostrar invalid DESPUES de que el usuario interactuo */
input:not(:placeholder-shown):invalid {
    border-color: var(--accent-red);
}

input:not(:placeholder-shown):valid {
    border-color: var(--accent-green);
}

/* Campos requeridos */
input:required {
    border-left: 3px solid var(--accent-blue);
}

/* Foco con teclado */
input:focus-visible {
    outline: 2px solid var(--accent-cyan);
    outline-offset: 2px;
}

novalidate y validación custom

Si necesitas validación 100% custom con JavaScript, agregá novalidate al <form> para desactivar la validación nativa. Luego usás los métodos del API: element.checkValidity(), element.validity.valid, element.validity.valueMissing, y element.setCustomValidity("mensaje") para mensajes personalizados. Así controlas todo el proceso sin que el navegador intervenga con sus mensajes por defecto.

Atributos de accesibilidad en formularios

Un formulario accesible no solo esusable por personas con discapacidades, sino que mejora la experiencia para todos. Los lectores de pantalla dependen de la estructura semántica del formulario para anunciar correctamente cada campo, y la navegación por teclado permite a cualquier usuario moverse eficientemente entre campos. Estos atributos y prácticas no agregan complejidad al código pero marcan una diferencia enorme.

HTML
<!-- aria-describedby: vincula con mensaje de ayuda -->
<label for="password">Contraseña</label>
<input type="password" id="password" name="password"
       aria-describedby="password-help">
<small id="password-help">
    Mínimo 8 caracteres con al menos una mayúscula.
</small>

<!-- aria-required vs required (uso ambos) -->
<label for="email">Email *</label>
<input type="email" id="email" name="email"
       required aria-required="true">

<!-- tabindex: orden de navegacion con teclado -->
<input type="text" name="nombre" tabindex="1">
<input type="email" name="email" tabindex="2">
<input type="password" name="password" tabindex="3">

<!-- autocomplete: ayuda al usuario y al navegador -->
<input type="text" name="nombre" autocomplete="name">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="telefono" autocomplete="tel">
<input type="text" name="direccion" autocomplete="street-address">
<input type="password" name="clave-nueva"
       autocomplete="new-password">

<!-- disabled vs readonly -->
<input type="text" value="No se puede editar" disabled>
<input type="text" value="Se puede leer pero no editar" readonly>
Atributo Propósito Cuándo usarlo
aria-describedby Vincula con texto de ayuda Siempre que un campo tenga instrucciones adicionales
aria-required Indica campo obligatorio Para lectores de pantalla (complementa required)
aria-invalid Indica error activo Validación custom JS (no con validación nativa)
autocomplete Ayuda al autocompletado del browser En todos los campos de datos personales
autofocus Foco automático al cargar Solo en el primer campo de un form (uno solo por página)
disabled Deshabilita totalmente el campo Campo no editable y no se envía con el form
readonly Solo lectura (se envía) Datos que se muestran pero no se pueden editar

Un solo autofocus por página

Usa autofocus en un solo campo por página, idealmente el primer input del formulario principal. Múltiples autofocus confunden al usuario (y al lector de pantalla) porque compiten por el foco. En móvil, el autofocus puede abrir el teclado automáticamente, lo que molesta si no es lo que el usuario espera. Considerá omitirlo en móvil con CSS o media queries.

Ejemplo: formulario de registro

Para cerrar, acá tenés un formulario de registro completo que usa todo lo que vimos: labels con for, fieldsets con legend, validación nativa, atributos de accesibilidad, tipos de input correctos, y una estructura semántica limpia. Es un ejemplo que podés usar como plantilla para tus propios formularios, adaptando los campos y validaciones a tus necesidades.

HTML
<form action="/registro" method="POST" novalidate>

    <!-- Datos personales -->
    <fieldset>
        <legend>Datos personales</legend>

        <div>
            <label for="nombre">Nombre completo *</label>
            <input type="text" id="nombre" name="nombre"
                   required minlength="2" maxlength="100"
                   autocomplete="name"
                   aria-required="true">
        </div>

        <div>
            <label for="email">Email *</label>
            <input type="email" id="email" name="email"
                   required
                   autocomplete="email"
                   aria-required="true"
                   aria-describedby="email-help">
            <small id="email-help">
                No compartiremos tu email con nadie.
            </small>
        </div>

        <div>
            <label for="password">Contraseña *</label>
            <input type="password" id="password" name="password"
                   required minlength="8"
                   pattern="(?=.*[A-Z])(?=.*\d).{8,}"
                   title="Mínimo 8 caracteres, una mayúscula y un número"
                   autocomplete="new-password"
                   aria-required="true"
                   aria-describedby="pass-help">
            <small id="pass-help">
                Mínimo 8 caracteres, al menos una mayúscula y un número.
            </small>
        </div>
    </fieldset>

    <!-- Preferencias -->
    <fieldset>
        <legend>Preferencias</legend>

        <div>
            <label for="pais">País</label>
            <select id="pais" name="pais">
                <option value="">Seleccioná un país</option>
                <option value="ar">Argentina</option>
                <option value="mx">México</option>
                <option value="es">España</option>
            </select>
        </div>

        <div>
            <label for="fecha-nac">Fecha de nacimiento</label>
            <input type="date" id="fecha-nac" name="fecha_nac"
                   min="1920-01-01" max="2010-12-31">
        </div>

        <div>
            <label>
                <input type="checkbox" name="newsletter" value="si">
                Quiero recibir el newsletter semanal
            </label>
        </div>
    </fieldset>

    <!-- Bio -->
    <div>
        <label for="bio">Sobre vos</label>
        <textarea id="bio" name="bio" rows="4"
                  maxlength="500"
                  placeholder="Contános algo sobre vos..."></textarea>
    </div>

    <!-- Botones -->
    <div>
        <button type="submit">Crear cuenta</button>
        <button type="reset">Limpiar</button>
    </div>

</form>

Tipos de botón en formularios

El atributo type del <button> define su comportamiento: submit envía el formulario (es el default), reset limpia todos los campos a sus valores iniciales, y button no hace nada por sí solo (ideal para botones que activan JavaScript). Siempre especificá el type explícitamente para evitar sorpresas, especialmente si el botón está dentro de un <form>.