learning-front

Nivel 1 · HTML y CSS: la estructura y la piel

Formularios y validación

Inputs, labels y validación nativa del navegador: mucho de lo que se hace con JavaScript lo da el HTML gratis si sabes pedirlo.

Con HTML semántico ya sabes estructurar una página para que tenga significado. Ahora llega la pieza que conecta el contenido con el usuario: los formularios. Son la forma en que la web recoge datos —un registro, una búsqueda, un pedido— y la mayoría de sus capacidades vienen del propio HTML, sin necesidad de JavaScript.

Cuando alguien rellena un formulario en tu aplicación, el navegador ya sabe muchas cosas: que un campo de email tiene que tener arroba, que un campo numérico no acepta letras, que un campo marcado como requerido no puede estar vacío. El problema es que eso no ocurre solo: tienes que pedírselo con el HTML correcto. Y la mayoría del tiempo no se pide.

Seguimos construyendo el Overwatch Team Builder. Esta vez añadimos el formulario para incorporar héroes a la plantilla: nombre, correo, estadísticas y rol. Sin tocar JavaScript.

El elemento <form> y por qué importa#

Un <form> no es un <div> con estilos de formulario. Tiene comportamiento propio: al pulsar el botón de tipo submit, recoge los valores de todos sus campos, los valida y los envía. Sin <form>, el botón no hace nada por defecto y la validación nativa no se activa.

html
<form action="/heroes" method="post">
  <!-- campos aquí -->
  <!-- type="submit" activa la validación y el envío -->
  <button type="submit">Añadir héroe</button>
</form>
<!-- action: URL a la que se envían los datos al pulsar el botón de envío -->
<!-- method: "post" envía los datos en el cuerpo de la petición (lo habitual para formularios) -->

action indica a dónde van los datos; method si se envían como GET (en la URL) o POST (en el cuerpo). En frontend puro, a menudo se intercepta el submit con JavaScript, pero el <form> sigue siendo el contenedor correcto: da semántica, activa validación y permite navegar por los campos con el teclado.

Tipos de <input>: el tipo correcto no es un detalle menor#

El atributo type le dice al navegador qué clase de dato espera el campo. La consecuencia más visible en 2026 es el teclado en móvil: type="email" muestra el teclado con @ a mano; type="number" muestra el teclado numérico; type="text" siempre muestra el teclado completo. Pero el tipo también activa validación automática:

html
<!-- El navegador valida que sea un email válido antes de enviar -->
<input type="email" name="contacto" />

<!-- Solo acepta enteros; min y max fijan el rango permitido -->
<input type="number" name="partidas" min="0" />

<!-- Selector de fecha nativo: sin librerías, sin JavaScript -->
<input type="date" name="fecha" />

Los tipos más usados en empresa: text, email, password, number, tel, url, date, checkbox, radio, file, hidden, submit. type="text" es el fallback cuando el navegador no reconoce el tipo, así que escribir el tipo correcto nunca rompe nada: en el peor caso lo trata como texto.

No todo es <input>. Para un texto largo —un comentario, una descripción— está <textarea>, que crece a varias líneas en vez de una; y para elegir de una lista cerrada de opciones, <select> con sus <option>. Ambos se asocian a su <label> igual que un <input>, con for e id.

html
<!-- textarea: campo de texto de varias líneas -->
<label for="notas">Notas del héroe</label>
<!-- rows fija la altura inicial visible -->
<!-- el contenido va entre la etiqueta de apertura y la de cierre, no en un atributo -->
<textarea id="notas" name="notas" rows="4">
</textarea>
html
<!-- select: lista cerrada de opciones; el usuario elige una -->
<label for="rol">Rol</label>
<!-- name es el nombre del dato que viaja al servidor -->
<select id="rol" name="rol">
  <!-- opción vacía como estado inicial -->
  <option value="">-- Elige un rol --</option>
  <!-- value es el dato que se envía; el texto es lo que ve el usuario -->
  <option value="tanque">Tanque</option>
  <option value="daño">Daño</option>
  <option value="apoyo">Apoyo</option>
</select>

Usa <textarea> cuando el usuario necesita escribir un texto largo con saltos de línea; usa <select> cuando las opciones son fijas y conocidas de antemano y el usuario debe elegir exactamente una.

Asociar <label> al campo: obligatorio, no opcional#

Un <label> sin vinculación no sirve de nada técnicamente. Hay dos formas de asociarlo.

La primera usa el atributo id. Un id es un identificador único: solo puede haber un elemento con ese valor en toda la página. El for del label y el id del input deben coincidir exactamente; así el navegador sabe que están enlazados.

html
<!-- Forma 1: for en el label, id en el input (los valores deben coincidir) -->
<label for="nombre-heroe">Nombre del héroe</label>
<input id="nombre-heroe" type="text" />

<!-- Forma 2: el label envuelve al input -->
<label>
  Nombre del héroe
  <input type="text" />
</label>

Las dos son válidas. La primera es más flexible (el label y el input pueden estar separados en el DOM); la segunda es más compacta. Lo que no funciona es usar solo un <span> o un <div> con texto, aunque visualmente parezca lo mismo.

Muchos inputs llevan también un atributo placeholder: es un texto de ejemplo que aparece dentro del campo mientras está vacío y desaparece en cuanto el usuario empieza a escribir. Sirve para dar una pista del formato esperado, pero no sustituye al <label>: el placeholder no es accesible como etiqueta y, al desaparecer, el usuario ya no sabe para qué es el campo si lo deja a medias.

El vínculo hace dos cosas:

  1. Área de clic ampliada: pulsar en el texto del label enfoca el input. En checkboxes y radios, donde el objetivo de clic es pequeño, esto es fundamental.
  2. Nombre accesible: el lector de pantalla anuncia “Nombre del héroe, campo de texto requerido”. Sin el vínculo, solo dice “campo de texto”.

Validación nativa: el navegador lo hace gratis#

Antes de que existiera JavaScript, los navegadores ya validaban formularios. Eso no ha cambiado; solo se ha mejorado. Los atributos clave:

html
<!-- Campo obligatorio: no se puede enviar vacío -->
<input type="text" required />

<!-- Email: valida el formato (algo@dominio.ext) -->
<input type="email" required />

<!-- Rango numérico -->
<input type="number" min="0" max="500" />

<!-- Longitud de texto: mínimo 2 caracteres, máximo 50 -->
<input type="text" minlength="2" maxlength="50" />

<!-- Patrón con expresión regular: solo letras (incluidas las del español) y espacios -->
<input type="text" pattern="[A-Za-záéíóúüñÁÉÍÓÚÜÑ\s]+" title="Solo letras y espacios" />

minlength y maxlength controlan la longitud en caracteres: minlength="2" impide enviar si el texto tiene menos de 2 caracteres; maxlength="50" impide escribir más de 50 (el campo deja de aceptar teclas al llegar al límite).

Cuando el usuario pulsa el botón de envío, el navegador comprueba todos estos atributos en orden. Si algo falla, muestra un mensaje de error nativo (localizado al idioma del sistema) y detiene el envío. El atributo title le dice qué mostrar cuando falla el pattern. Nada de esto necesita JavaScript. Lo que sí necesita JavaScript es personalizar el aspecto de esos mensajes o validaciones más complejas (que victorias no supere a partidas, por ejemplo).

<fieldset> y <legend>: agrupar campos relacionados#

Cuando varios campos forman una pregunta (un grupo de radios, un conjunto de checkboxes), <fieldset> y <legend> los envuelven con nombre:

html
<fieldset>
  <legend>Rol</legend>
  <!-- Los tres radios comparten el mismo name="rol": eso los hace un grupo.
       Solo se puede seleccionar uno a la vez dentro del mismo grupo.
       El name también es el nombre con el que el dato viaja al servidor al enviar. -->
  <label><input type="radio" name="rol" value="tanque" /> Tanque</label>
  <label><input type="radio" name="rol" value="daño" /> Daño</label>
  <label><input type="radio" name="rol" value="apoyo" /> Apoyo</label>
</fieldset>

El atributo name en los radios cumple dos funciones: los radios con el mismo name forman un grupo (el navegador solo permite seleccionar uno a la vez dentro de ese grupo); y name es el nombre con el que el dato elegido viaja al servidor cuando se envía el formulario.

Sin el <fieldset>, el lector de pantalla anuncia “Tanque, radio” sin contexto. Con él, anuncia “Rol, grupo de opciones — Tanque, radio”. El usuario sabe qué está eligiendo.

El borde visual por defecto del <fieldset> suele ser feo; en empresa casi siempre se neutraliza con CSS (border: none). CSS es el lenguaje que controla el aspecto —lo verás a partir del capítulo siguiente— pero no hace falta entenderlo ahora: el elemento sigue ahí, invisible para los ojos, imprescindible para quien lo necesita.

Accesibilidad: un poco más allá del label#

Con labels y tipos correctos ya cubres el 80% de la accesibilidad de formularios. Para el 20% restante, dos atributos que merece la pena conocer:

aria-describedby vincula un campo con un elemento que contiene información de ayuda. El lector de pantalla la lee después del nombre del campo, sin mezclarla con el label:

html
<label for="victorias">Victorias</label>
<input id="victorias" type="number" min="0" aria-describedby="victorias-hint" />
<p id="victorias-hint">No puede superar el número de partidas jugadas.</p>

autocomplete le dice al navegador (y a los gestores de contraseñas) qué clase de dato espera cada campo, para que pueda autocompletar correctamente:

html
<input type="email" autocomplete="email" />
<input type="text" autocomplete="name" />

Los valores de autocomplete están estandarizados; los más usados: name, email, tel, username, current-password, new-password, street-address, postal-code.

Pruébalo#

El formulario de abajo está bien construido. Intenta enviarlo con campos vacíos, con un email mal formado o con un número de partidas negativo. El navegador avisa. Sin JavaScript. Después observa el código de la izquierda: cada label tiene su for, cada input su id, los radios están dentro de un fieldset. Cambia required por nada en el nombre y comprueba que el formulario ya acepta enviarse sin él.

Verás también atributos class= y un fichero styles.css: son CSS, el lenguaje que controla el aspecto. Lo aprenderás en el capítulo siguiente; por ahora ignóralos y céntrate en la estructura del formulario.

Comprueba lo que sabes#

Pregunta 1 de 5

Tu formulario tiene un `<input>` para el email. ¿Qué ventaja real aporta usar `type="email"` frente a `type="text"`?

Tu turno#

Tienes el formulario a medio hacer, aquí mismo. Los TODOs están señalados con comentarios en el HTML. El CSS está dado y no se toca. Tres pasadas: que funcione, que valide bien, que sea accesible del todo. Edita el HTML en el playground; cuando creas que lo tienes, despliega las soluciones y compáralas con la tuya.

Ejercicio · en esta página

Completa el formulario del Team Builder

En starter/index.html tienes el esqueleto de un formulario para añadir un héroe a la plantilla. Tiene TODOs señalados con comentarios. El CSS ya está dado y no debes tocarlo. Resuélvelo en tres pasadas: primero que funcione, luego que valide bien, luego que sea accesible del todo.

Paso 1: Que funcione

  • El formulario se ve y se puede enviar.
  • Todos los campos tienen texto legible (aunque sea con placeholder en lugar de label).
  • Los tipos son todos text: el navegador no valida nada por sí solo.
Ver soluciones
<!doctype html>
<!-- SOLUCIÓN OK ─────────────────────────────────────────────────────────────
  El formulario se ve bien y se puede enviar.
  Limitaciones:
  - Los labels no están asociados a sus inputs: un clic en el texto no activa
    el campo, y un lector de pantalla no sabe a qué campo corresponde.
  - Todos los inputs son type="text": el móvil no cambia el teclado, el
    navegador no valida el formato del email ni de los números.
  - Sin validación nativa: el formulario acepta un email inválido o victorias
    negativas sin protestar.
  - Los radios no están agrupados; no hay forma de saber que forman una opción.
──────────────────────────────────────────────────────────────────────────── -->
<html lang="es">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Añadir héroe — Team Builder</title>
    <link rel="stylesheet" href="../../starter/styles.css" />
  </head>
  <body>
    <div class="page">

      <div class="site-header">
        <div class="brand">Overwatch <span>Team Builder</span></div>
      </div>

      <div>
        <div class="section-title">Añadir héroe a la plantilla</div>

        <form class="hero-form" action="#" method="post">

          <div class="field">
            <span class="field-label">Nombre del héroe</span>
            <input class="field-input" type="text" placeholder="Nombre" />
          </div>

          <div class="field">
            <span class="field-label">Correo de contacto</span>
            <input class="field-input" type="text" placeholder="correo@ejemplo.com" />
          </div>

          <div class="field">
            <span class="field-label">Partidas jugadas</span>
            <input class="field-input" type="text" placeholder="0" />
          </div>

          <div class="field">
            <span class="field-label">Victorias</span>
            <input class="field-input" type="text" placeholder="0" />
          </div>

          <div class="field">
            <span class="field-label">Rol</span>
            <div class="radio-group">
              <input type="radio" name="rol" value="tanque" />
              <span>Tanque</span>
              <input type="radio" name="rol" value="daño" />
              <span>Daño</span>
              <input type="radio" name="rol" value="apoyo" />
              <span>Apoyo</span>
            </div>
          </div>

          <div class="field field--check">
            <input class="check-input" type="checkbox" />
            <span class="check-label">Confirmo que los datos son correctos</span>
          </div>

          <button class="btn-submit" type="submit">Añadir héroe</button>

        </form>
      </div>

      <div class="site-footer">
        <p>Overwatch Team Builder &copy; 2026</p>
      </div>

    </div>
  </body>
</html>

Por qué este nivel

  • El formulario funciona: se ve bien, los campos están ahí y se puede enviar.
  • Sin labels asociados: un clic en "Nombre del héroe" no activa el input; el lector de pantalla solo anuncia "campo de texto" sin decir para qué sirve.
  • Todos los tipos son text: el móvil no cambia el teclado para el email, y el navegador acepta "abc" como número de partidas sin protestar.
  • Los radios están sueltos; sin fieldset/legend no hay forma de saber que forman una pregunta concreta.