Guía Web

HTML, CSS y JavaScript, explicados y listos para pedir

Apuntes visuales, catálogo de capacidades y manual de peticiones a la IA — todo en una sola página, en español e inglés, sin conexión necesaria.

Esta página tiene tres usos: aprender (cada concepto está explicado para alguien que nunca ha programado), consultar (qué existe exactamente en HTML, CSS y JavaScript, con ejemplos funcionando en vivo) y pedir (cada tarjeta incluye la frase exacta, en lenguaje natural, con la que pedirle ese elemento a una IA como Claude). Usa el índice o el buscador para saltar a cualquier punto, y el botón «Copiar» de cada frase 🗣 para llevártela a tu conversación con la IA.

1 Índice completo

Todo el contenido de la página, ordenado y enlazado. Escribe en el buscador para filtrar en vivo: el índice se reduce a lo que coincida con tu búsqueda.

3 HTML — la estructura

HTML es el esqueleto de toda página web: define qué es cada cosa (un título, un párrafo, un botón), no cómo se ve. Cada tarjeta de esta sección sigue el mismo patrón de cuatro partes: qué es (explicado sin tecnicismos), el código, una demo en vivo funcionando de verdad y la frase 🗣 con la que pedírselo a una IA hablando.

Base del documento

La plantilla mínima: <!DOCTYPE>, <html lang>, <head> y <body>

Todo documento HTML empieza igual, como una carta empieza con fecha y saludo. <!DOCTYPE html> le dice al navegador «esto es HTML moderno»; lang="es" declara el idioma (clave para lectores de pantalla y traductores); en el <head> va lo invisible (título de la pestaña, codificación de caracteres, ajuste a pantallas móviles) y en el <body> todo lo que se ve. meta charset="UTF-8" garantiza que tildes y eñes se muestren bien, y meta viewport hace que la página no se vea diminuta en el móvil.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mi página</title>
</head>
<body>
  <!-- Aquí va todo el contenido visible -->
</body>
</html>

Demo en vivo

Esta misma página usa exactamente esa plantilla. Datos reales de este documento:

  • Idioma declarado:
  • Codificación:
  • Título de la pestaña:

«Créame la estructura básica de una página HTML en español, preparada para verse bien en el móvil y con el título "Mi página" en la pestaña.»

Estructura semántica

Las etiquetas semánticas dicen qué papel juega cada zona de la página. Es la diferencia entre una casa con habitaciones rotuladas (cocina, baño, dormitorio) y una nave diáfana llena de cajas iguales: el navegador, los buscadores y los lectores de pantalla entienden la primera al instante.

<header> — la cabecera

Es la parte de arriba de la página (o de una sección): logo, título y navegación principal. Como la portada de una revista, presenta lo que viene. No confundir con <head>, que es invisible y solo contiene metadatos.

<header>
  <h1>Panadería Sol</h1>
  <p>Pan artesano desde 1982</p>
</header>

Demo en vivo

Panadería Sol

Pan artesano desde 1982

«Ponle a la página una cabecera con el nombre del negocio bien grande y un lema debajo.»

<nav> — la navegación

Agrupa los enlaces principales para moverse por el sitio, como el índice de un libro o los carteles de un centro comercial. Los lectores de pantalla permiten saltar directamente a ella, así que úsala solo para navegación de verdad, no para cualquier lista de enlaces.

<nav aria-label="Principal">
  <ul>
    <li><a href="#inicio">Inicio</a></li>
    <li><a href="#productos">Productos</a></li>
    <li><a href="#contacto">Contacto</a></li>
  </ul>
</nav>

Demo en vivo

«Añade un menú de navegación con enlaces a Inicio, Productos y Contacto que me lleven a cada sección de la página al pulsarlos.»

<main> — el contenido principal

Envuelve el contenido central y único de la página: lo que el visitante vino a ver. Solo puede haber uno por página. Es el escenario del teatro: cabecera y pie son el marco, <main> es la función.

<body>
  <header>…</header>
  <main>
    <h1>Nuestros panes</h1>
    <p>Horneados cada mañana…</p>
  </main>
  <footer>…</footer>
</body>

Demo en vivo

header
main — contenido principal
footer

«Organiza la página con su cabecera arriba, el contenido principal en el centro y el pie abajo, usando las etiquetas semánticas correctas.»

<section> y <article> — bloques con sentido

<section> agrupa contenido relacionado bajo un mismo tema, como los capítulos de un libro; casi siempre lleva su propio título. <article> es una pieza completa y autónoma que tendría sentido por sí sola fuera de la página: una noticia, una receta, una tarjeta de producto, una reseña. Truco: si podrías compartirlo suelto por WhatsApp y se entendería, es un <article>.

<section>
  <h2>Recetas de la semana</h2>
  <article>
    <h3>Pan de centeno</h3>
    <p>Ingredientes: harina, agua, sal…</p>
  </article>
  <article>
    <h3>Hogaza rústica</h3>
    <p>Fermentación lenta de 12 horas…</p>
  </article>
</section>

Demo en vivo

Recetas de la semana

Pan de centeno

Ingredientes: harina, agua, sal…

Hogaza rústica

Fermentación lenta de 12 horas…

«Crea una sección de recetas con su título, y dentro cada receta como una tarjeta independiente con su nombre y su descripción.»

<aside> — contenido secundario

Contenido relacionado pero prescindible: la nota al margen de un libro de texto. Barras laterales, datos curiosos, enlaces relacionados, publicidad. Si lo quitas, el contenido principal sigue entendiéndose igual.

<article>
  <h2>Cómo hacer masa madre</h2>
  <p>El proceso empieza mezclando…</p>
  <aside>
    <p>💡 ¿Sabías que…? La masa madre más antigua
    documentada tiene más de 120 años.</p>
  </aside>
</article>

Demo en vivo

El proceso empieza mezclando harina y agua a partes iguales…

«Junto al artículo, añade un recuadro lateral con un dato curioso que se distinga visualmente del texto principal.»

<figure> y <figcaption> — imagen con pie

Una imagen (o gráfico, o bloque de código) con su leyenda, unidas oficialmente. Como la foto de un periódico con su pie explicativo: si las separas, la foto pierde contexto. El navegador y los lectores de pantalla saben que ese texto describe esa imagen.

<figure>
  <img src="hogaza.jpg" alt="Hogaza de pan recién horneada">
  <figcaption>Nuestra hogaza rústica, premio local 2025.</figcaption>
</figure>

Demo en vivo

Nuestra hogaza rústica, premio local 2025.

«Inserta la foto del producto con un pie de foto debajo que diga qué es, de forma que imagen y texto queden asociados.»

<details> y <summary> — el desplegable sin JavaScript

Un bloque que se abre y se cierra al pulsar, sin una sola línea de JavaScript. Perfecto para preguntas frecuentes o información opcional. <summary> es el texto siempre visible que hace de botón; el resto aparece al desplegarlo. Funciona con teclado por sí solo.

<details>
  <summary>¿Hacéis envíos a domicilio?</summary>
  <p>Sí, de martes a sábado por la mañana,
  con pedido mínimo de 10 €.</p>
</details>

Demo en vivo

¿Hacéis envíos a domicilio?

Sí, de martes a sábado por la mañana, con pedido mínimo de 10 €.

¿Tenéis opciones sin gluten?

Sí, horneadas en obrador separado los jueves.

«Añade una sección de preguntas frecuentes donde cada pregunta se despliegue al pulsarla para mostrar su respuesta, sin usar JavaScript.»

Texto y contenido

<h1><h6> — los títulos y su jerarquía

Seis niveles de título, como el esquema de unos apuntes: <h1> es el tema del día (solo uno por página), <h2> los grandes apartados, <h3> los subapartados… La regla de oro: no saltarse niveles (de h2 a h4) y no elegir el título por su tamaño — el tamaño se cambia con CSS, el nivel dice la importancia. Los lectores de pantalla navegan saltando de título en título, así que una buena jerarquía es un buen mapa.

<h1>Panadería Sol</h1>          <!-- único en la página -->
  <h2>Nuestros panes</h2>
    <h3>De masa madre</h3>
    <h3>Integrales</h3>
  <h2>Dónde estamos</h2>

Demo en vivo

Panadería Sol

Nuestros panes

De masa madre

Integrales

Dónde estamos

«Organiza los títulos de la página en jerarquía: un único título principal y luego apartados y subapartados bien anidados, sin saltarse niveles.»

<p> — el párrafo

La unidad básica de texto: cada idea, su párrafo. El navegador añade espacio entre ellos automáticamente. Dentro puedes resaltar con <strong> (importancia real, se lee con énfasis) y <em> (énfasis de entonación), que no son lo mismo que «negrita» y «cursiva» decorativas.

<p>Abrimos <strong>todos los días</strong> a las 7:00,
incluso <em>festivos</em>.</p>
<p>Los domingos solo hasta mediodía.</p>

Demo en vivo

Abrimos todos los días a las 7:00, incluso festivos.

Los domingos solo hasta mediodía.

«Escribe la información en párrafos cortos y resalta en negrita solo lo verdaderamente importante, como el horario.»

<ol>, <ul> y <li> — listas ordenadas y sin orden

<ul> es la lista de la compra (el orden da igual, con viñetas); <ol> es la receta (los pasos van numerados y el orden importa). Cada elemento va en su <li>. Se pueden anidar listas dentro de listas para crear subniveles.

<h3>Pasos</h3>
<ol>
  <li>Mezclar harina y agua</li>
  <li>Reposar 30 minutos</li>
  <li>Añadir la sal</li>
</ol>

<h3>Necesitarás</h3>
<ul>
  <li>Un bol grande</li>
  <li>Una báscula</li>
</ul>

Demo en vivo

Pasos

  1. Mezclar harina y agua
  2. Reposar 30 minutos
  3. Añadir la sal

Necesitarás

  • Un bol grande
  • Una báscula

«Pon los pasos de la receta como lista numerada y los ingredientes como lista de puntos.»

<a> — el enlace

La pieza que convirtió los documentos en «web»: un texto pulsable que lleva a otro lugar. href es el destino (otra página, una sección de esta misma con #id, un correo con mailto:). target="_blank" lo abre en pestaña nueva — y cuando se usa, conviene añadir rel="noopener noreferrer" por seguridad y privacidad. El texto del enlace debe decir a dónde va: «ver horarios» sí, «pincha aquí» no.

<a href="#contacto">Ir a contacto</a>

<a href="https://ejemplo.com" target="_blank"
   rel="noopener noreferrer">
  Sitio externo (pestaña nueva)
</a>

<a href="mailto:hola@ejemplo.com">Escríbenos</a>

«Añade un enlace que lleve a la sección de contacto, y otro a la web del proveedor que se abra en una pestaña nueva de forma segura.»

<img> — la imagen

Inserta una imagen. Es una etiqueta «vacía»: no se cierra. src es la ruta del archivo y alt es el texto alternativo: lo que se lee en voz alta a personas ciegas y lo que aparece si la imagen no carga. Escribe en alt lo que dirías por teléfono para describir la foto; si es puramente decorativa, se deja vacío (alt="") para que el lector de pantalla la salte.

<img src="imagenes/hogaza.jpg"
     alt="Hogaza de pan sobre una tabla de madera"
     width="400" height="300">

Demo en vivo

Hogaza de pan sobre una tabla de madera (ilustración de ejemplo)

Imagen SVG incrustada (sin archivos externos). Su texto alternativo: «Hogaza de pan sobre una tabla de madera».

«Inserta la foto de la hogaza con un texto alternativo que la describa para personas que usan lector de pantalla, y evita que la página salte mientras carga.»

<table> — la tabla de datos

Para datos que se leen en filas y columnas, como una hoja de cálculo: precios, horarios, comparativas. <caption> es su título, <th> las celdas de cabecera (con scope para que el lector de pantalla sepa a qué se refiere cada dato) y <td> los datos. Importante: las tablas son para datos, nunca para maquetar la página — para eso está CSS.

<table>
  <caption>Precios del pan</caption>
  <thead>
    <tr><th scope="col">Producto</th><th scope="col">Precio</th></tr>
  </thead>
  <tbody>
    <tr><td>Hogaza</td><td>3,50 €</td></tr>
    <tr><td>Baguette</td><td>1,20 €</td></tr>
  </tbody>
</table>

Demo en vivo

Precios del pan
ProductoPrecio
Hogaza3,50 €
Baguette1,20 €

«Monta una tabla de precios con dos columnas, producto y precio, con su fila de cabecera y accesible para lectores de pantalla.»

Formularios a fondo

El formulario es la conversación entre tu página y quien la visita: la web pregunta, la persona responde y los datos viajan al servidor. Es la zona donde más se nota la diferencia entre una página amateur y una profesional, así que la tratamos pieza a pieza.

<form> — el contenedor y sus atributos

Es el sobre que agrupa los campos y sabe a dónde enviarlos. Sus atributos son las instrucciones de envío: action (la dirección del servidor que recibe los datos), method (get pone los datos en la URL — solo para búsquedas; post los envía en privado — para todo lo demás), enctype="multipart/form-data" (obligatorio si se suben archivos), autocomplete (permite al navegador rellenar datos conocidos), novalidate (desactiva la validación nativa — útil solo para probar la tuya), target (dónde abrir la respuesta), name (identifica el formulario), rel (relación con el destino) y accept-charset (codificación del envío, casi siempre UTF-8).

<form action="/procesar" method="post"
      enctype="multipart/form-data"
      autocomplete="on" accept-charset="UTF-8">
  <!-- campos… -->
  <button type="submit">Enviar</button>
</form>

Demo en vivo

Al enviar, esta demo muestra los datos que viajarían al servidor (aquí el envío está simulado: no sale nada de tu navegador).

«Crea un formulario que envíe los datos de forma privada al servidor, preparado para subir archivos y con autocompletado del navegador activado.»

<label> — la etiqueta del campo

El rótulo visible que dice qué va en cada campo. Se asocia con su campo mediante for (en el label) e id (en el campo): al asociarlos, pulsar el texto activa el campo — regalo enorme en móvil — y el lector de pantalla anuncia el nombre correcto al entrar en él. Regla de oro: todo campo tiene su label visible; el placeholder no lo sustituye, porque desaparece al escribir.

<label for="correo">Correo electrónico</label>
<input type="email" id="correo" name="correo">

Demo en vivo

«Pon a cada campo del formulario su etiqueta visible encima, bien asociada, para que al tocar el texto se active el campo.»

<fieldset> y <legend> — grupos de campos

Agrupan campos relacionados bajo un título común, como los apartados de un impreso oficial («Datos personales», «Dirección de envío»). Imprescindible con radios y checkboxes: la <legend> da el contexto («¿Tamaño?») que cada opción individual no tiene. Un lector de pantalla lee «¿Tamaño? — Grande», no solo «Grande».

<fieldset>
  <legend>Tamaño de la hogaza</legend>
  <label><input type="radio" name="tam" value="m"> Mediana</label>
  <label><input type="radio" name="tam" value="g"> Grande</label>
</fieldset>

Demo en vivo

Tamaño de la hogaza

«Agrupa las opciones de tamaño en un recuadro con su título, para que se entienda que pertenecen a la misma pregunta.»

Todos los tipos de <input>

Un mismo elemento, <input>, se transforma en 22 controles distintos según su atributo type. Elegir el tipo correcto te regala validación, teclado móvil adecuado y controles nativos (calendario, paleta de color…) sin escribir nada más.

type="text"texto libre

El campo comodín: una línea de texto sin reglas. Para nombres, apellidos, ciudades… Si existe un tipo más específico (email, tel…), úsalo antes que este.

<input type="text" name="nombre">

«Añade al formulario un campo de texto para el nombre, con su etiqueta visible.»

type="email"correo electrónico

Exige formato de correo (algo@algo) sin que escribas ninguna regla, y en el móvil muestra el teclado con la @ a mano.

<input type="email" name="correo" required>

«Añade un campo de correo electrónico obligatorio que avise si el formato no es válido.»

type="password"contraseña

Oculta lo escrito con puntos para que nadie lo lea por encima del hombro. Ojo: solo lo oculta en pantalla; la seguridad real la pone el servidor y el candado HTTPS.

<input type="password" name="clave"
       minlength="8" autocomplete="new-password">

«Añade un campo de contraseña de mínimo 8 caracteres, con un botón para mostrarla u ocultarla.»

type="tel"teléfono

En el móvil abre el teclado numérico de marcar. No valida el formato por sí solo (cada país es distinto): combínalo con pattern si necesitas uno concreto.

<input type="tel" name="telefono"
       pattern="[0-9 +]{9,15}">

«Añade un campo de teléfono que en el móvil abra el teclado numérico y acepte solo números, espacios y el signo más.»

type="url"dirección web

Exige una dirección completa (con https://) y en móvil añade teclas como «.com» y «/». Útil para «tu página web» en perfiles.

<input type="url" name="web"
       placeholder="https://…">

«Añade un campo para la página web del cliente que compruebe que es una dirección válida y completa.»

type="number"número

Solo acepta números, con flechitas para subir y bajar. min, max y step definen los límites y el salto. Para códigos que «parecen» números pero no lo son (código postal), mejor texto con inputmode="numeric".

<input type="number" name="cantidad"
       min="1" max="12" step="1" value="1">

«Añade un campo de cantidad que solo acepte números del 1 al 12, con flechas para subir y bajar.»

type="range"deslizador

Un control deslizante para valores aproximados (volumen, presupuesto). Como no muestra el número elegido, acompáñalo siempre de un <output> que lo enseñe en vivo.

<input type="range" name="picor"
       min="0" max="10" value="5">
<output>5</output>
5

«Añade un deslizador del 0 al 10 que muestre al lado el valor elegido mientras se mueve.»

type="date"fecha

Abre el calendario nativo del dispositivo al pulsarlo: cero errores de formato. min y max acotan las fechas elegibles (por ejemplo, nada anterior a hoy).

<input type="date" name="recogida"
       min="2026-08-03">

«Añádele al formulario un campo de fecha de nacimiento que muestre un calendario al pulsarlo y que sea obligatorio.»

type="time"hora

Selector de hora y minutos con el control nativo. Con step puedes limitar a intervalos (900 = cada 15 minutos).

<input type="time" name="hora"
       min="07:00" max="20:00" step="900">

«Añade un selector de hora entre las 7:00 y las 20:00, en intervalos de 15 minutos.»

type="datetime-local"fecha y hora

Fecha y hora juntas en un solo control, en la zona horaria del usuario. Ideal para citas y reservas.

<input type="datetime-local" name="cita">

«Añade un campo para elegir día y hora de la cita en un mismo control.»

type="month"mes y año

Selector de mes + año, sin día. Típico de la caducidad de tarjetas o informes mensuales.

<input type="month" name="caducidad">

«Añade un campo para elegir mes y año, sin día concreto, como el de caducidad de una tarjeta.»

type="week"semana

Selecciona una semana concreta del año (semana 32 de 2026). Poco frecuente, pero perfecto para planificaciones semanales. El soporte varía entre navegadores: comprueba cómo se ve en el tuyo.

<input type="week" name="semana">

«Añade un selector de semana del año para planificar el turno, y dime qué pasa en navegadores que no lo soporten.»

type="color"color

Abre la paleta de colores del sistema y devuelve el color en formato hexadecimal (#rrggbb). Útil para personalizaciones.

<input type="color" name="color" value="#0e7466">

«Deja que el usuario elija un color con la paleta del sistema y usa ese color en la vista previa.»

type="file"archivo

Abre el selector de archivos del dispositivo. accept limita los tipos (por ejemplo, solo imágenes), multiple permite varios y en móvil capture puede abrir directamente la cámara. El formulario necesita enctype="multipart/form-data" para enviarlos.

<input type="file" name="foto"
       accept="image/*" multiple>

«Añade un botón para adjuntar fotos, que solo acepte imágenes y permita seleccionar varias a la vez.»

type="checkbox"casilla

Casillas independientes: puedes marcar ninguna, una o varias. Como marcar platos en un menú. Para «acepto las condiciones», una casilla obligatoria con required.

<label><input type="checkbox" name="extra"
  value="semillas"> Con semillas</label>

Extras (marca los que quieras)

«Añade casillas para elegir extras, permitiendo marcar varios a la vez, y una casilla obligatoria de aceptar las condiciones.»

type="radio"opción única

Opciones excluyentes: elegir una desmarca las demás, como los botones de una radio antigua. El truco está en el name: todas las opciones del mismo grupo comparten el mismo.

<label><input type="radio" name="pago"
  value="tarjeta"> Tarjeta</label>
<label><input type="radio" name="pago"
  value="efectivo"> Efectivo</label>

Forma de pago (solo una)

«Añade las formas de pago como opciones donde solo se pueda elegir una, con la pregunta como título del grupo.»

type="hidden"campo oculto

Un dato que viaja con el formulario sin que el usuario lo vea ni lo toque: el número interno del producto, un código de campaña. Como la etiqueta cosida por dentro de una prenda. Nunca guardes ahí secretos: cualquiera puede verlo mirando el código de la página.

<input type="hidden" name="id-producto"
       value="HOGAZA-42">

Aquí hay un campo oculto de verdad con el valor «HOGAZA-42». No se ve: esa es su función. Viajaría con el envío del formulario.

«Haz que el formulario envíe también el código del producto sin mostrárselo al usuario.»

type="submit"enviar

El botón que dispara el envío del formulario y su validación. Pulsar Enter en un campo también lo activa. Hoy se prefiere <button type="submit">, que admite iconos y contenido dentro.

<input type="submit" value="Enviar pedido">
<!-- equivalente moderno -->
<button type="submit">Enviar pedido</button>

«Pon al final del formulario un botón de enviar bien visible que valide los campos antes de mandar nada.»

type="reset"restablecer

Devuelve todos los campos del formulario a su valor inicial. Úsalo con cuidado: un clic accidental borra todo lo escrito y frustra al usuario. En formularios largos, mejor evitarlo o pedir confirmación.

<input type="reset" value="Vaciar formulario">

«Añade un botón secundario para vaciar el formulario, menos llamativo que el de enviar y que pida confirmación antes de borrar.»

type="button"botón neutro

Un botón que no envía nada por sí mismo: espera a que JavaScript le dé un trabajo (abrir un menú, sumar una cantidad…). Dentro de un <form>, todo botón sin type es «submit» por accidente: decláralo siempre.

<input type="button" value="+1">
<!-- o su forma moderna -->
<button type="button">+1</button>

Contador de barras: cada clic suma

0

«Pon un botón que al pulsarlo sume una unidad al contador, sin enviar el formulario.»

type="image"botón-imagen

Una imagen que actúa como botón de envío; además manda las coordenadas exactas del clic. Es una reliquia de los mapas de imagen: hoy casi siempre es mejor un <button> con un icono dentro, pero conviene reconocerla al leer código antiguo.

<input type="image" src="boton-enviar.png"
       alt="Enviar pedido">

Esta imagen envía el formulario al pulsarla:

«Explícame la diferencia entre un botón-imagen antiguo y un botón moderno con icono, y usa la opción moderna en mi formulario.»

Los demás controles de formulario

<textarea> — texto de varias líneas

El folio en blanco del formulario: para mensajes, comentarios y cualquier texto largo. rows sugiere su altura inicial y maxlength limita los caracteres. A diferencia del input, se cierra con etiqueta de cierre, y lo que pongas entre ambas es su contenido inicial.

<label for="mensaje">Tu mensaje</label>
<textarea id="mensaje" name="mensaje" rows="4"
          maxlength="500" required></textarea>

Demo en vivo

0/500

«Añade un cuadro grande para escribir el mensaje, con límite de 500 caracteres y un contador en vivo de los que quedan.»

<select>, <option> y <optgroup> — el desplegable

Una lista cerrada de opciones donde elegir, como la carta de un restaurante: solo puedes pedir lo que está. <optgroup> agrupa opciones bajo subtítulos («Panes», «Dulces»); selected marca la opción inicial y multiple permite elegir varias (con Ctrl). Consejo: para 2-4 opciones, los radios se ven de un vistazo; el desplegable gana cuando la lista es larga.

<label for="producto">¿Qué quieres encargar?</label>
<select id="producto" name="producto">
  <optgroup label="Panes">
    <option value="hogaza">Hogaza</option>
    <option value="baguette" selected>Baguette</option>
  </optgroup>
  <optgroup label="Dulces">
    <option value="croissant">Croissant</option>
  </optgroup>
</select>

Demo en vivo

«Pon un desplegable para elegir el producto, con las opciones agrupadas en panes y dulces y una opción marcada por defecto.»

<button> — el botón moderno

El elemento estrella para acciones. Sus tres personalidades según type: submit (envía el formulario — es el valor por defecto, ¡cuidado!), reset (restablece) y button (neutro, para JavaScript). Admite texto, iconos y HTML dentro, y es accesible por teclado sin hacer nada. Regla: si lleva a otra página es un enlace <a>; si hace algo, es un botón.

<button type="submit">Enviar pedido</button>
<button type="button" disabled>Agotado</button>

Demo en vivo

«Crea un botón principal bien destacado, otro secundario más discreto y muéstrame cómo queda uno deshabilitado.»

<datalist> — sugerencias mientras escribes

El híbrido entre campo libre y desplegable: el usuario puede escribir lo que quiera, pero recibe sugerencias que se filtran con cada letra. Se conecta al input con list apuntando al id del datalist. Autocompletado nativo sin una línea de JavaScript.

<label for="ciudad">Ciudad de entrega</label>
<input id="ciudad" name="ciudad" list="ciudades">
<datalist id="ciudades">
  <option value="Madrid"></option>
  <option value="Barcelona"></option>
  <option value="Valencia"></option>
</datalist>

Demo en vivo

«Haz que el campo de ciudad sugiera opciones mientras se escribe, pero permitiendo también escribir una que no esté en la lista.»

<output> — el resultado de un cálculo

La pantalla de la calculadora: un hueco donde mostrar el resultado que depende de otros campos. Los lectores de pantalla lo tratan como región viva: anuncian el cambio sin que el usuario tenga que ir a buscarlo.

<form>
  <input type="number" id="uds" value="2"> ×
  <input type="number" id="precio" value="3.50"> € =
  <output id="total">7,00 €</output>
</form>

Demo en vivo

× € = 7,00 €

Cambia las cifras: el total se recalcula solo.

«Haz que al cambiar la cantidad se recalcule el precio total automáticamente y se muestre al lado, sin recargar la página.»

<progress> — la barra de progreso

Cuánto queda de una tarea: subir un archivo, completar un formulario por pasos. value es lo hecho y max el total. Sin value se vuelve «indeterminada»: se anima sola para decir «trabajando, no sé cuánto queda».

<label for="subida">Subiendo foto…</label>
<progress id="subida" value="66" max="100">66 %</progress>

Demo en vivo

0 %

«Muestra una barra de progreso mientras se sube el archivo, que avance de verdad y anuncie cuando llegue al cien por cien.»

<meter> — el medidor de nivel

Un valor dentro de una escala conocida, como el indicador de gasolina: no mide avance sino estado. Con low, high y optimum el navegador lo colorea solo (verde/ámbar/rojo). Ideal para fuerza de contraseña, ocupación de aforo o espacio en disco. No confundir con <progress>: progress avanza, meter mide.

<label for="aforo">Aforo del taller</label>
<meter id="aforo" value="18" min="0" max="20"
       low="10" high="16" optimum="5">18 de 20</meter>

Demo en vivo

Aforo casi lleno (18/20): 18/20
Aforo tranquilo (4/20): 4/20

«Añade un medidor de fuerza de la contraseña que cambie de color según sea débil, aceptable o fuerte.»

Atributos de los controles

Los atributos son los ajustes finos de cada campo: qué es obligatorio, qué formato se admite, qué teclado sale en el móvil… Aquí están todos los importantes, agrupados por lo que hacen.

Reglas de validación: required, minlength, maxlength, min, max, step, pattern

Las normas que un campo exige antes de dejarte enviar, como el portero de una sala: required (no puede quedar vacío), minlength/maxlength (largo del texto), min/max/step (límites y salto de los números y fechas) y pattern (un patrón exacto, tipo «5 dígitos» para el código postal). El navegador comprueba todo esto solo, sin JavaScript.

<input type="text" name="cp" required
       pattern="[0-9]{5}"
       title="Cinco dígitos, por ejemplo 28001">

<input type="number" name="uds" min="1" max="10" step="1">

<input type="text" name="usuario"
       minlength="3" maxlength="15">

Demo en vivo

«Haz obligatorio el código postal, que solo admita exactamente cinco dígitos y que el navegador explique el formato cuando esté mal.»

Estado y contenido: value, placeholder, readonly, disabled, checked, selected, multiple, size

Definen cómo llega el campo al usuario: value (contenido inicial), placeholder (pista gris que desaparece al escribir — nunca sustituye al label), readonly (se ve y se envía, pero no se edita), disabled (apagado: ni se edita ni se envía), checked/selected (opción marcada de inicio), multiple (varias selecciones o archivos) y size (ancho visible en caracteres). La diferencia readonly/disabled importa: el número de pedido es readonly (debe enviarse), el cupón caducado está disabled (no debe contar).

<input type="text" name="pedido" value="P-2026-081"
       readonly>

<input type="text" name="cupon" value="VERANO24"
       disabled>

<input type="text" name="alias"
       placeholder="Ej.: Lucía G.">

Demo en vivo

«Muestra el número de pedido en un campo que se vea pero no se pueda editar, y deja el campo del cupón desactivado hasta que haya promociones.»

Ayudas de escritura: inputmode, autocomplete, autofocus, list, accept, capture

Hacen que escribir cueste menos: inputmode elige el teclado del móvil sin cambiar el tipo de campo (numeric para el código postal), autocomplete le dice al navegador qué dato es para que lo rellene él (name, email, postal-code…), autofocus coloca el cursor en un campo al cargar (úsalo solo en páginas cuyo único fin es ese formulario), list conecta con un <datalist> de sugerencias, y accept/capture controlan qué archivos se admiten y si se abre la cámara.

<input type="text" name="cp" inputmode="numeric"
       autocomplete="postal-code">

<input type="email" name="correo" autocomplete="email">

<input type="file" name="foto" accept="image/*" capture="environment">

Demo en vivo

«Configura los campos para que en el móvil salga el teclado adecuado en cada uno y el navegador pueda autocompletar el nombre, el correo y la dirección.»

Controles fuera del formulario: form, formaction, formmethod, formnovalidate, formtarget

Casos avanzados que conviene conocer: el atributo form permite que un campo o botón viva fuera del <form> pero pertenezca a él (útil en maquetas complejas), y los atributos formaction/formmethod/formnovalidate/formtarget en un botón concreto sobrescriben la configuración del formulario solo para ese botón. El uso clásico: un botón «Guardar borrador» que envía a otra dirección y sin validar, junto al «Enviar» normal.

<form id="pedido" action="/enviar" method="post">
  <input type="text" name="nombre" required>
  <button type="submit">Enviar</button>
  <button type="submit" formaction="/borrador"
          formnovalidate>Guardar borrador</button>
</form>

<!-- Este campo está FUERA pero pertenece al formulario -->
<input type="text" name="nota" form="pedido">

Demo en vivo

«Añade junto al botón de enviar otro de guardar borrador que no valide los campos y mande los datos a otra dirección.»

Validación nativa de HTML

Cómo valida el navegador (y cómo se conecta con CSS y JS)

Cuando pulsas «Enviar», el navegador repasa cada campo contra sus reglas (required, type, pattern…). Si algo falla, bloquea el envío, enfoca el primer campo erróneo y muestra un globo con el mensaje en el idioma del navegador. Ese estado se refleja en CSS (:valid / :invalid pintan el campo) y JavaScript puede leerlo y personalizarlo con la API de validación (checkValidity(), setCustomValidity(), la propiedad validity). La estrategia profesional tiene tres capas: HTML valida lo básico, JS mejora los mensajes en tiempo real y el servidor revalida siempre (la única capa que un tramposo no puede saltarse).

<form>
  <label for="correo-v">Correo</label>
  <input type="email" id="correo-v" name="correo" required>
  <button type="submit">Enviar</button>
</form>
<!-- Sin JS: el navegador bloquea, enfoca y explica solo -->

Demo en vivo

El globo de error que ves lo genera tu navegador, en su idioma, sin una línea de JavaScript.

«Valida el formulario en tres capas: reglas nativas de HTML, mensajes personalizados en tiempo real con JavaScript, y déjame comentado dónde iría la validación del servidor.»

Accesibilidad en formularios (ARIA)

ARIA es un juego de atributos que completa lo que HTML no puede decir por sí solo. La primera regla de ARIA es no usar ARIA cuando HTML ya lo hace: un <button> no necesita role="button". Se usa solo para lo que falta.

aria-label y aria-describedby — nombre y descripción

aria-label da nombre a un control que no tiene texto visible: el botón que solo es una ✕ o una lupa. Sin él, el lector de pantalla dice «botón» a secas. aria-describedby añade información extra apuntando al id de otro texto: la ayuda «mínimo 8 caracteres» bajo la contraseña se lee justo después del nombre del campo. Nombre = qué es; descripción = detalles.

<button type="button" aria-label="Cerrar ventana">✕</button>

<label for="clave2">Contraseña</label>
<input type="password" id="clave2" name="clave"
       aria-describedby="clave2-ayuda">
<p id="clave2-ayuda">Mínimo 8 caracteres, con una mayúscula
y un número.</p>

Demo en vivo

← Un lector de pantalla dice «Cerrar ventana, botón», no solo «✕».

Mínimo 8 caracteres, con una mayúscula y un número.

«Pon nombre accesible a los botones que solo tienen icono y asocia el texto de ayuda de cada campo para que el lector de pantalla lo lea al entrar.»

aria-invalid, aria-live y role — estados y anuncios

Tres piezas para que los cambios dinámicos no pasen desapercibidos: aria-invalid="true" marca oficialmente un campo como erróneo (los lectores lo anuncian al entrar); aria-live="polite" convierte una zona en «altavoz»: cualquier texto que aparezca ahí se lee en voz alta sin mover el foco — perfecto para «Formulario enviado ✓» o los errores de validación; y role reetiqueta un elemento solo cuando no existe etiqueta HTML nativa para ese papel (por ejemplo, role="alert" para un aviso urgente que debe leerse ya). Si existe la etiqueta nativa, úsala y olvida el role.

<input type="email" id="c3" name="correo"
       aria-invalid="true" aria-describedby="c3-err">
<p id="c3-err" role="alert">El correo no es válido.</p>

<!-- Zona de anuncios: lo que aparezca aquí, se lee -->
<p aria-live="polite" id="estado-envio"></p>

Demo en vivo

Al pulsar, el mensaje aparece en una región aria-live: quien use lector de pantalla lo oye sin tener que buscarlo.

«Haz que los errores y el mensaje de éxito del formulario se anuncien automáticamente a los lectores de pantalla, marcando además cada campo erróneo como inválido.»

4 CSS — el aspecto

Si HTML es el esqueleto, CSS es la piel, la ropa y la forma de moverse: colores, tamaños, disposición y animación. Mismo patrón de tarjetas: explicación llana, código, demo visual en vivo y la frase 🗣 para pedirlo.

Anatomía y cascada

La regla CSS: selector, propiedad y valor

Toda regla CSS es una frase con la misma gramática: a quién (el selector), qué ajustar (la propiedad) y cómo dejarlo (el valor). «Todos los botones (selector) tendrán fondo (propiedad) verde (valor)». Las declaraciones van entre llaves y cada una termina en punto y coma.

/*  selector  {  propiedad: valor;  }  */
button {
  background-color: #0e7466;
  color: white;
  border-radius: 8px;
}

Demo en vivo

buttonselector { backgroundpropiedad: #0e7466valor; }

El resultado de esa regla, aplicado de verdad:

«Dale a todos los botones fondo verde oscuro, letra blanca y esquinas redondeadas.»

Cascada y especificidad: quién gana cuando dos reglas chocan

Cuando dos reglas quieren pintar el mismo elemento, gana la más específica: el ID (#) puede más que la clase (.), y la clase puede más que la etiqueta. A igual especificidad, gana la que está escrita más abajo (por eso «cascada»). Es como las señales de tráfico: la norma general cede ante la señal de la calle concreta, y esta ante la del guardia. Consejo profesional: trabaja casi siempre con clases y mantén la especificidad baja; los ID y !important son atajos que luego se pagan.

p        { color: gray; }    /* especificidad baja   */
.aviso   { color: #b3762a; } /* la clase gana a la etiqueta */
#urgente { color: #b3362c; } /* el ID gana a la clase */

Demo en vivo

Párrafo normal: me pinta la regla de etiqueta (gris).

Tengo la clase .aviso: la clase gana a la etiqueta (ámbar).

Tengo clase Y el id #urgente: el ID gana a todo (rojo).

«Revisa por qué este texto no coge el color que le puse y arréglalo bajando la especificidad, sin usar !important.»

Selectores

Selectores básicos: etiqueta, clase, ID, universal y atributo

Cinco formas de señalar «a quién»: la etiqueta (p) alcanza a todos los párrafos; la clase (.destacado) a los que tú marques — es la herramienta del día a día; el ID (#cabecera) a un único elemento; el universal (*) a todo; y el de atributo ([type="email"]) a los que tengan cierto atributo o valor. Pulsa cada selector en la demo y mira a quién selecciona.

p                { }  /* todos los <p> */
.destacado       { }  /* clase: los marcados por ti */
#unico           { }  /* ID: exactamente uno */
*                { }  /* todo */
[type="email"]   { }  /* por atributo */

Demo interactiva — pulsa un selector

Un párrafo normal.

Un párrafo con la clase .destacado.

Párrafo con clase y además el id #unico.

«Aplica el estilo destacado solo a los elementos que yo marque con una clase, sin tocar el resto de párrafos.»

Combinadores: descendiente, hijo >, hermanos + y ~

Seleccionan según el parentesco, como un árbol genealógico: el espacio (article p) alcanza a cualquier descendiente, por profundo que esté; > solo a los hijos directos; + al hermano pegado justo después; y ~ a todos los hermanos posteriores. Pruébalos en la demo.

article p    { }  /* p dentro de article (a cualquier nivel) */
article > p  { }  /* p hijo directo de article */
h4 + p       { }  /* el p inmediatamente después de un h4 */
h4 ~ p       { }  /* todos los p hermanos posteriores al h4 */

Demo interactiva — pulsa un selector

article

— un h4 (título)

— p hermano justo después del h4

— otro p hermano posterior

div

— p nieto (dentro del div)

«Haz que el primer párrafo que va justo después de cada título se vea como entradilla, más grande y en gris, sin añadir clases a mano.»

Selectores funcionales modernos: :is(), :where(), :not(), :has()

Atajos con lógica: :is(h1, h2, h3) agrupa varios selectores en uno; :where() hace lo mismo pero sin sumar especificidad (ideal para estilos base fáciles de sobrescribir); :not(.vendido) selecciona a los que NO cumplen algo; y :has() es el histórico «selector de padre»: article:has(img) selecciona los artículos que contienen una imagen. :has() funciona en todos los navegadores modernos (desde finales de 2023); para páginas que deban soportar navegadores viejos, comprueba el soporte con @supports.

:is(h1, h2, h3) { line-height: 1.2; }

.lista li:not(.vendido) { opacity: 1; }

/* tarjetas que contienen imagen: a dos columnas */
article:has(img) { display: grid; }

Demo interactiva — pulsa un selector

Hogaza clásica (clase .a)

Croissant — .vendido (agotado)

Baguette (clase .b) ¡novedad!

Palmera, sin clase especial

«Atenúa todos los productos menos los agotados, y resalta solo las tarjetas que contengan la etiqueta de novedad, sin añadir clases nuevas al HTML.»

Pseudo-clases: estados y posiciones

Estados de interacción: :hover, :focus, :focus-visible, :active, :visited

Un mismo elemento vive varios momentos y cada uno puede tener su estilo: :hover (el ratón está encima), :focus (está seleccionado, por clic o teclado), :focus-visible (el foco llegó por teclado — es donde debe verse el anillo de foco, sin molestar al clic), :active (justo mientras se pulsa) y :visited (enlace ya visitado). Estos estados son el «lenguaje corporal» de la interfaz: dicen «soy pulsable», «estás aquí», «te he oído».

.boton:hover         { background: #0a5a50; }
.boton:focus-visible { outline: 3px solid #0e7466;
                       outline-offset: 2px; }
.boton:active        { transform: scale(0.97); }
a:visited            { color: #7a5ea6; }

Demo en vivo — pasa el ratón, navega con Tab, mantén pulsado

«Dale al botón sus cuatro estados: que se oscurezca con el ratón encima, que se hunda un poco al pulsarlo y que al navegar con teclado se vea un anillo de foco claro.»

Estados de formulario: :checked, :disabled, :required, :valid, :invalid y compañía

Los campos también cuentan su vida a CSS: :checked (casilla o radio marcados), :disabled (apagado), :required/:optional (obligatorio o no), :valid/:invalid (¿cumple sus reglas ahora mismo?), :placeholder-shown (aún no se ha escrito nada), :read-only (no editable) e :indeterminate (el estado «ni sí ni no» de una casilla madre con hijas a medias). Con ellos, el formulario da feedback visual sin JavaScript.

input:invalid:not(:placeholder-shown) {
  border-color: #b3362c;          /* error solo tras escribir */
}
input:valid:not(:placeholder-shown) {
  border-color: #2c7a3f;          /* verde cuando está bien */
}
input[type="checkbox"]:checked + span { font-weight: 600; }

Demo en vivo

«Que cada campo se ponga con borde verde cuando esté bien rellenado y rojo cuando esté mal, pero solo después de que el usuario haya empezado a escribir.»

Posición en la familia: :first-child, :last-child, :nth-child()

Seleccionan por orden de nacimiento: el primero, el último, o siguiendo un patrón — :nth-child(odd) los impares (el clásico «cebra» de las tablas), :nth-child(3n) cada tercero. Sirven para dar estilo según posición sin ensuciar el HTML con clases.

li:first-child     { font-weight: 600; }
li:last-child      { border-bottom: none; }
tr:nth-child(odd)  { background: #f3f1ec; }

Demo en vivo

  • Primero — en negrita por :first-child
  • Segundo — fondo cebra por :nth-child(odd/even)
  • Tercero
  • Cuarto — fondo cebra
  • Último — sin línea inferior por :last-child

«Pinta las filas de la tabla alternando color de fondo, tipo cebra, y quita la línea de separación de la última.»

Pseudo-elementos

::before, ::after, ::placeholder, ::selection, ::marker

Si las pseudo-clases seleccionan momentos, los pseudo-elementos crean o estilizan partes: ::before y ::after añaden contenido decorativo antes y después (etiquetas, iconos, flechas) sin tocar el HTML; ::placeholder da estilo al texto de pista; ::selection cambia el color al seleccionar texto; y ::marker personaliza las viñetas y números de las listas.

.nuevo::before  { content: "✦ "; color: #0e7466; }
.oferta::after  { content: " — 20 %"; font-weight: 700; }
input::placeholder { color: #9a938a; font-style: italic; }
::selection     { background: #0e7466; color: white; }
li::marker      { color: #0e7466; }

Demo en vivo

Producto nuevo (la ✦ la pone ::before)

Hogaza en oferta (el descuento lo añade ::after)

Selecciona este texto con el ratón: la selección va en color de acento.

  • Viñeta con ::marker en color de acento
  • Otra viñeta personalizada

«Ponle a cada producto nuevo una estrellita delante y a las ofertas el porcentaje de descuento detrás, sin que yo tenga que escribirlo en el HTML.»

Variables CSS (custom properties)

--variable y var() — el sistema visual en un solo sitio

Las variables CSS son el bote de pintura etiquetado: defines una vez «--color-acento: verde» y lo usas en cien sitios con var(--color-acento). ¿Cambio de imagen corporativa? Tocas una línea y toda la web se repinta. Se declaran normalmente en :root (toda la página) y son la base de los temas claro/oscuro: cada tema redefine las mismas variables con otros valores — exactamente lo que hace esta guía. La demo usa variables de verdad: cambia el acento y observa.

:root {
  --acento: #0e7466;
  --radio: 10px;
  --espacio: 1rem;
}
.tarjeta {
  border: 2px solid var(--acento);
  border-radius: var(--radio);
  padding: var(--espacio);
}

Demo interactiva — cambia la variable --acento

Tarjeta que usa var(--acento)

Un solo cambio de variable repinta borde, título y botón a la vez.

«Define los colores, espaciados y radios de toda la web como variables CSS en un solo sitio, de forma que cambiar la paleta entera sea tocar tres líneas.»

Layout: Flexbox y Grid

Flexbox — alinear en una dirección

Flexbox coloca elementos en fila o en columna y reparte el espacio, como ordenar libros en una balda: puedes pegarlos a la izquierda, centrarlos, separarlos por igual… display: flex activa el modo en el contenedor; justify-content reparte en el eje principal, align-items alinea en el perpendicular, flex-direction elige fila o columna y gap pone la separación. Es la herramienta para barras de navegación, filas de botones y cualquier alineación «en una dirección». Pulsa los valores y observa el efecto real:

.contenedor {
  display: flex;
  flex-direction: row;            /* fila o columna */
  justify-content: space-between; /* reparto del espacio */
  align-items: center;            /* alineación transversal */
  gap: 1rem;                      /* separación entre piezas */
}

Demo interactiva — combina valores

justify-content

align-items

flex-direction · gap

1
2
3

display: flex; justify-content: flex-start; align-items: stretch; flex-direction: row; gap: 0.75rem;

«Coloca el logo a la izquierda y el menú a la derecha en la misma línea, centrados en vertical, y que en móvil se apilen en columna.»

Grid — la cuadrícula en dos direcciones

Grid dibuja una cuadrícula de filas y columnas y coloca cada pieza en su celda, como una estantería con casillas: la herramienta para galerías de productos, paneles y la estructura general de la página. grid-template-columns define las columnas (1fr = una parte proporcional del espacio); el mágico repeat(auto-fit, minmax(120px, 1fr)) crea columnas que se recolocan solas según el ancho — responsive sin media queries. Una pieza puede ocupar varias celdas con grid-column: span 2. Regla mental: alineación en una dirección → Flexbox; cuadrícula en dos → Grid.

.galeria {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}
.destacada { grid-column: span 2; } /* ocupa 2 columnas */

Demo interactiva — cambia las columnas

grid-template-columns

1
2
3
4
5
6

display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem;

«Monta la galería de productos en una cuadrícula que en el ordenador muestre tres columnas y en el móvil una, con la primera tarjeta ocupando el doble de ancho.»

Responsive: adaptarse a cada pantalla

@media — media queries y breakpoints

Una media query es una regla condicional: «si la pantalla mide al menos tanto, aplica estos estilos». Los anchos donde el diseño cambia se llaman breakpoints; unos razonables: móvil (hasta 599 px), tablet (600–899 px) y escritorio (900 px en adelante). La estrategia profesional es móvil primero: los estilos base son para pantalla pequeña y las media queries van añadiendo mejoras al crecer — así el móvil, que es el entorno más exigente, nunca queda como un remiendo. Esta misma guía funciona así.

/* Base: móvil (sin media query) */
.galeria { grid-template-columns: 1fr; }

/* Tablet */
@media (min-width: 600px) {
  .galeria { grid-template-columns: repeat(2, 1fr); }
}

/* Escritorio */
@media (min-width: 900px) {
  .galeria { grid-template-columns: repeat(3, 1fr); }
}

Demo en vivo

Ahora mismo tu ventana está en modo:

Estrecha o ensancha la ventana del navegador (o gira el móvil) y verás cambiar la etiqueta: la pinta una media query real, sin JavaScript.

«Diseña primero para móvil y añade puntos de corte para tablet y ordenador, de forma que la galería pase de una a dos y a tres columnas.»

Unidades relativas: rem, em, %, vw, ch

Los píxeles son centímetros fijos; las unidades relativas son «tallas» que se adaptan: rem se mide respecto al tamaño de letra base de la página (si el usuario la agranda en ajustes, todo escala con ella — por eso es la unidad reina para textos y espaciados), em respecto al elemento actual, % respecto al padre, vw/vh respecto a la ventana (50vw = mitad del ancho de pantalla) y ch respecto al ancho de un carácter (ideal para limitar líneas de texto a ~65ch, el largo cómodo de lectura).

html   { font-size: 100%; }   /* respeta el ajuste del usuario */
h1     { font-size: 2rem; }   /* 2 × la base */
p      { max-width: 65ch; }   /* líneas cómodas de leer */
.panel { width: 90%; }        /* fluido respecto al padre */

Demo en vivo

200px — fijo: no se adapta
50% — la mitad de su contenedor
20rem — crece si el usuario agranda la letra
30vw — un tercio del ancho de la ventana

«Usa unidades relativas en textos y espaciados para que la web escale bien si el usuario agranda la letra, y limita los párrafos a un ancho cómodo de lectura.»

Reglas @ y preferencias del usuario

@supports — preguntar antes de usar

Es el «¿tu navegador sabe hacer esto?» de CSS: el bloque solo se aplica si el navegador soporta la propiedad o el selector indicado. Permite usar novedades con red de seguridad: primero escribes la versión clásica para todos y luego, dentro de @supports, la mejora moderna para quien pueda.

/* base para todos los navegadores */
.tarjeta { border: 1px solid gray; }

/* mejora solo si :has() está soportado */
@supports selector(:has(a)) {
  .tarjeta:has(img) { border-color: teal; }
}

Demo en vivo

Si ves este recuadro en verde con un ✓, tu navegador soporta :has() — lo ha comprobado @supports, no JavaScript.

«Usa esa propiedad moderna de CSS, pero con una alternativa clásica para navegadores que no la soporten, comprobándolo con @supports.»

@keyframes y animation — animaciones con guion

Mientras una transición va «de A a B», una animación sigue un guion completo por fotogramas clave: qué pasa al 0 %, al 50 %, al 100 %. Se define con @keyframes y se asigna con animation (duración, ritmo, repeticiones). Para spinners de carga, apariciones suaves y pequeños acentos — siempre con moderación y respetando la preferencia de movimiento reducido.

@keyframes latido {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.15); }
}
.corazon {
  animation: latido 1.2s ease-in-out infinite;
}

Demo en vivo

¡Aparezco con @keyframes!

«Añade un indicador de carga animado y haz que las tarjetas aparezcan con un fundido suave, desactivando las animaciones para quien tenga configurado movimiento reducido.»

prefers-color-scheme y prefers-reduced-motion — escuchar al sistema

El sistema operativo del usuario ya sabe dos cosas importantes: si prefiere tema oscuro y si el movimiento le marea. CSS puede preguntarlo con dos media queries: prefers-color-scheme: dark (aplicar la paleta oscura por defecto) y prefers-reduced-motion: reduce (apagar animaciones y desplazamientos suaves). Respetarlas es señal inequívoca de trabajo profesional; esta guía lo hace en toda la página.

@media (prefers-color-scheme: dark) {
  :root { --fondo: #191612; --texto: #ece7de; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important;
      transition: none !important; }
}

Demo en vivo

  • Tu sistema prefiere:
  • Movimiento reducido:

Valores leídos de tu sistema ahora mismo. Cámbialos en los ajustes de tu dispositivo y recarga para verlos cambiar.

«Haz que la web arranque en tema oscuro si el sistema del usuario lo prefiere, y que todas las animaciones se desactiven si tiene activado el movimiento reducido.»

Estilado completo de formularios

Inputs, selects, checkboxes y mensajes con estilo propio

Los controles nativos llegan con el «uniforme» de cada navegador; vestirlos con tu diseño es lo que da aspecto de producto: bordes y radios coherentes, foco visible con el color de acento, checkboxes y radios ampliados con accent-color (una línea y adoptan tu paleta), el botón interno del input de archivo estilizado con ::file-selector-button, y mensajes de error y éxito con color + icono (nunca solo color: hay quien no distingue rojo de verde). Todos los campos de esta guía usan este sistema; la demo lo concentra.

input, select, textarea {
  border: 1.5px solid var(--borde);
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
  min-height: 44px;                 /* táctil */
}
input:focus-visible { outline: 3px solid var(--acento); }
input[type="checkbox"], input[type="radio"] {
  accent-color: var(--acento);
  width: 1.15rem; height: 1.15rem;
}
input[type="file"]::file-selector-button { /* botón interno */ }
.error { color: var(--rojo); }
.exito { color: var(--verde); }

Demo en vivo

Así se ve un mensaje de error: color + icono.

Y así uno de éxito, distinguible sin ver el color.

«Dale a todos los campos del formulario un estilo coherente con la web: bordes suaves, foco bien visible, casillas del color de la marca y mensajes de error con icono además de color.»

Transiciones y microinteracciones

transition — cambios con suavidad

Una transición hace que un cambio de estilo ocurra en el tiempo en vez de a saltos: el botón no «teletransporta» su color al pasar el ratón, lo funde en 200 milisegundos. Se declara qué propiedad, cuánto dura y con qué ritmo (ease, linear…). La dosis correcta: 150–300 ms en hover y foco; más de medio segundo en una interacción cotidiana ya estorba. Transiciona transform y opacity (van fluidas); evita animar anchos y altos (obligan al navegador a recalcular la página).

.tarjeta {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.tarjeta:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 20px rgba(0,0,0,0.12);
}

Demo en vivo — pasa el ratón por las tarjetas

Con transición (0.2s): elegante

Sin transición: brusca

«Añade microinteracciones sutiles: que las tarjetas se eleven un poco al pasar el ratón y los botones cambien de color con una transición suave de unos 200 milisegundos.»

Accesibilidad visual

Contraste, foco visible y tamaño táctil

Tres reglas que deciden si tu web se puede usar en el mundo real: contraste — el texto normal necesita una relación de al menos 4.5:1 con su fondo (piensa en leer la pantalla al sol); foco visible — quien navega con teclado necesita ver dónde está, así que el anillo de foco jamás se elimina (el clásico outline: none sin reemplazo es de los errores más dañinos que existen); y tamaño táctil — todo lo pulsable debe medir al menos 44×44 px, el tamaño de una yema de dedo.

/* foco visible siempre (nunca outline:none sin reemplazo) */
:focus-visible {
  outline: 3px solid var(--acento);
  outline-offset: 2px;
}
/* zona táctil mínima */
button, a.boton { min-height: 44px; min-width: 44px; }

Demo en vivo

Contraste correcto: se lee sin esfuerzo (≈12:1)

Contraste insuficiente: gris sobre gris (≈2:1) — evítalo

«Revisa el contraste de todos los textos para que cumplan el mínimo de accesibilidad, asegúrate de que el foco de teclado se vea siempre y de que todos los botones tengan tamaño táctil suficiente.»

5 JavaScript — el comportamiento

JavaScript es lo que convierte el documento en aplicación: reacciona a clics, valida datos, pide información y cambia la página sin recargarla. Cada tarjeta trae un botón Ejecutar: el código corre de verdad en tu navegador y el resultado aparece debajo.

Fundamentos

let y const — cajas para guardar valores

Una variable es una caja con etiqueta donde guardas un valor para usarlo después. const es una caja precintada: lo que metes no se puede reemplazar (úsala por defecto; el 90 % de las veces no necesitas cambiar el valor). let es la caja abierta para lo que sí cambia (un contador, el texto que escribe el usuario). La vieja var existe, pero tiene comportamientos traicioneros: no la uses en código nuevo.

const nombre = "Lucía";   // no cambiará
let puntos = 0;           // cambiará
puntos = puntos + 10;     // ✓ permitido con let
// nombre = "Ana";        // ✗ error: const no se reasigna

Demo en vivo


      

«Guarda el nombre del usuario y sus puntos en variables, usando constantes para lo que no cambie, y ve sumando puntos cada vez que acierte.»

Tipos de datos: texto, número, booleano, null y undefined

Cada valor tiene un tipo, como cada ingrediente tiene su envase: string (texto, entre comillas), number (números, con o sin decimales), boolean (solo true o false — el interruptor), null («vacío a propósito») y undefined («aún sin valor»). Conocer el tipo evita el clásico susto: "2" + 2 es "22" (pega textos), no 4.

const texto = "Hola";        // string
const precio = 3.5;          // number
const abierto = true;        // boolean
const descuento = null;      // vacío a propósito
let proximoPedido;           // undefined: sin valor aún

typeof precio;               // "number"
"2" + 2;                     // "22" ¡cuidado: pega textos!
Number("2") + 2;             // 4 ✓ convertido antes

Demo en vivo


      

«Asegúrate de convertir a número lo que venga de los campos del formulario antes de hacer cuentas, para que no se peguen como texto.»

Operadores: aritmética, comparación y lógica

Las herramientas de calcular y decidir: aritméticos (+ - * / %, donde % es «el resto de la división»: perfecto para saber si un número es par), de comparación (usa siempre === y !==, que comparan sin hacer trampas de conversión; evita ==) y lógicos (&& «y», || «o», ! «no») para combinar condiciones: «si es socio Y el pedido supera 20 €».

const total = 3.5 * 4;        // 14
const esPar = 14 % 2 === 0;   // true (resto 0)

5 === "5";                    // false: distinto tipo ✓ fiable
5 == "5";                     // true… por eso NO se usa ==

const socio = true, pedido = 25;
const envioGratis = socio && pedido > 20;  // true

Demo en vivo


      

«Calcula el total del pedido y aplica envío gratis solo si el cliente es socio y el importe supera los 20 euros.»

Condicionales: if, else y el ternario

La bifurcación del camino: «si pasa esto, haz A; si no, haz B». if / else if / else encadena todas las alternativas que necesites. El operador ternario (condición ? A : B) es su versión de bolsillo para elegir entre dos valores en una línea. Prueba la demo con tu edad:

if (edad < 18) {
  mensaje = "Menú infantil disponible";
} else if (edad >= 65) {
  mensaje = "Descuento senior aplicado";
} else {
  mensaje = "Menú estándar";
}

// Ternario: dos opciones en una línea
const saludo = esDeDia ? "Buenos días" : "Buenas noches";

Demo en vivo


      

«Según la edad que escriba el usuario, muéstrale el menú infantil, el estándar o el descuento senior, cada uno con su mensaje.»

Bucles: for, for...of y while

Repetir sin copiar y pegar: el bucle es la cadena de montaje. for clásico cuando controlas el contador; for...of para recorrer una lista elemento a elemento («para cada producto de la lista…»), y while cuando no sabes cuántas vueltas harán falta («mientras quede masa, haz panes»).

// for clásico: contador bajo control
for (let i = 1; i <= 3; i++) {
  console.log("Horneada nº " + i);
}

// for...of: recorre una lista
const panes = ["hogaza", "baguette", "centeno"];
for (const pan of panes) {
  console.log("Listo: " + pan);
}

Demo en vivo


      

«Recorre la lista de productos y crea una tarjeta en la página por cada uno, sin repetir código a mano.»

Funciones y arrow functions — recetas con nombre

Una función es una receta guardada: la escribes una vez y la ejecutas cuando quieras, con distintos ingredientes (parámetros) y un resultado (return). Las arrow functions (=>) son la escritura moderna y compacta, omnipresente en el código actual: mismas ideas, menos ceremonia. Si un dato de entrada puede faltar, dale un valor por defecto.

// Función clásica
function precioConIva(precio, iva = 21) {
  return precio * (1 + iva / 100);
}

// La misma, como arrow function
const precioConIva2 = (precio, iva = 21) =>
  precio * (1 + iva / 100);

precioConIva(10);     // 12.1
precioConIva2(10, 4); // 10.4 (iva reducido)

Demo en vivo


      

«Organiza el código en funciones pequeñas y reutilizables con nombres claros, cada una con una sola responsabilidad, y usa la sintaxis moderna de flecha.»

Arrays y sus métodos: map, filter, find, forEach

Un array es una lista ordenada de valores, y sus métodos son operaciones de cinta transportadora: map transforma cada elemento y devuelve la lista transformada (precios → precios con IVA); filter deja pasar solo los que cumplen la condición (solo los disponibles); find devuelve el primero que encaje (busca por nombre); y forEach hace algo con cada uno sin devolver nada (pintarlos en pantalla). Encadenados, son media programación moderna: productos.filter(...).map(...).

const panes = [
  { nombre: "Hogaza",   precio: 3.5, hay: true  },
  { nombre: "Baguette", precio: 1.2, hay: true  },
  { nombre: "Centeno",  precio: 4.0, hay: false }
];

const disponibles = panes.filter(p => p.hay);
const nombres = disponibles.map(p => p.nombre);
const caro = panes.find(p => p.precio > 3);

nombres;       // ["Hogaza", "Baguette"]
caro.nombre;   // "Hogaza"

Demo en vivo


      

«De la lista de productos, quédate solo con los disponibles, súbeles el IVA y muéstralos ordenados por precio.»

Objetos — fichas con propiedades

Si el array es una lista, el objeto es una ficha: pares de nombre y valor que describen una cosa (un cliente tiene nombre, correo y si es socio). Se accede con punto (cliente.nombre) y la desestructuración extrae varias propiedades de golpe. La combinación estrella del desarrollo real: un array de objetos — la lista de fichas.

const cliente = {
  nombre: "Lucía",
  correo: "lucia@ejemplo.com",
  socia: true
};

cliente.nombre;              // "Lucía"
cliente.telefono = "600...", // añadir propiedad

// Desestructuración: extraer de golpe
const { nombre, socia } = cliente;

Demo en vivo


      

«Guarda los datos de cada cliente como un objeto con nombre, correo y si es socio, y muéstrame cómo leer y actualizar sus propiedades.»

Template literals — texto con huecos

Las comillas invertidas (`) crean textos con huecos que se rellenan solos: ${variable} inserta el valor dentro de la frase. Adiós a pegar trocitos con +. Además respetan los saltos de línea, perfectos para generar HTML desde JavaScript.

const nombre = "Lucía";
const total = 12.5;

// Antes (pegando con +):
const v1 = "Hola " + nombre + ", total: " + total + " €";

// Ahora (template literal):
const v2 = `Hola ${nombre}, total: ${total} €`;
const v3 = `Con cálculo dentro: ${total * 1.21} € con IVA`;

Demo en vivo


      

«Monta el mensaje de confirmación insertando el nombre del cliente y el total dentro de la frase, con la sintaxis moderna de plantillas.»

try / catch — la red de seguridad

Cuando una operación puede fallar (interpretar datos, llamar a un servidor), try intenta el plan A y catch recoge el error si algo estalla, en vez de dejar que rompa la página entera. Es el airbag del código: no evita el golpe, pero evita el desastre. Dentro del catch se avisa al usuario con un mensaje comprensible, no con jerga técnica.

try {
  const datos = JSON.parse(textoRecibido); // puede fallar
  mostrarPedido(datos);
} catch (error) {
  // Plan B: avisar sin romper la página
  mostrarMensaje("No pudimos leer el pedido. Inténtalo de nuevo.");
}

Demo en vivo


      

«Envuelve las operaciones que puedan fallar en try/catch y, si algo va mal, muestra al usuario un mensaje claro en su idioma en vez de romper la página.»

Promesas y async/await — esperar sin congelar

Hay tareas que tardan (pedir datos a un servidor, leer un archivo). Una promesa es el resguardo de la tintorería: te lo dan al momento y con él recoges el resultado cuando esté — o te enteras de que algo falló. async/await es la forma moderna de usarlas: await significa «espera aquí a que llegue, sin congelar la página». Mientras tanto, el usuario ve un estado de carga. Combinado con try/catch, cubre éxito y error con naturalidad.

async function cargarPedido() {
  mostrarCargando(true);
  try {
    const respuesta = await fetch("/api/pedido"); // espera
    const datos = await respuesta.json();          // espera
    mostrarPedido(datos);                          // éxito
  } catch (error) {
    mostrarError("No se pudo cargar el pedido");   // fallo
  } finally {
    mostrarCargando(false);                        // siempre
  }
}

Demo en vivo


      

«Cuando pidas los datos al servidor, muestra un indicador de carga mientras tanto, usa async/await y contempla también el caso de que la petición falle.»

El DOM: JavaScript toca la página

¿Qué es el DOM? El árbol vivo de la página

Cuando el navegador lee tu HTML, construye en memoria un árbol genealógico de la página: el DOM (Document Object Model). Cada etiqueta es una rama; <body> es el tronco y cada párrafo, botón o imagen cuelga de su padre. JavaScript no edita tu archivo HTML: edita este árbol en vivo, y el navegador repinta al instante. Por eso una página puede cambiar sin recargarse.

// El árbol, simplificado:
// document
// └── html
//     ├── head → title, meta…
//     └── body
//         ├── header → h1, nav
//         ├── main   → section, p, form…
//         └── footer

document.title;            // lee del árbol
document.body.children;    // las ramas hijas del body

Demo en vivo


      

«Explícame con un dibujo cómo queda el árbol DOM de mi página y qué partes va a tocar el JavaScript que me propones.»

querySelector y querySelectorAll — encontrar elementos

Los prismáticos para localizar ramas del árbol: usan la misma sintaxis que los selectores CSS que ya conoces. querySelector devuelve el primer elemento que encaja; querySelectorAll devuelve todos (una lista que se recorre con forEach). Encontrar el elemento es siempre el paso 1: después se lee, se cambia o se escucha.

const titulo = document.querySelector("h1");
const boton  = document.querySelector("#enviar");
const cartas = document.querySelectorAll(".tarjeta");

cartas.forEach(carta => {
  carta.classList.add("visible");
});

Demo en vivo

Tarjeta 1 (clase .qs-item)

Tarjeta 2 (clase .qs-item)

Tarjeta 3 (clase .qs-item)


      

«Localiza todas las tarjetas de producto de la página y aplícales el cambio a la vez, usando selectores como los de CSS.»

Crear, modificar y eliminar elementos

JavaScript puede fabricar ramas nuevas y colgarlas del árbol: createElement crea el elemento, textContent le pone el texto (siempre seguro), append lo cuelga y remove() lo poda. Nota de seguridad: para insertar texto del usuario usa textContent, no innerHTML — lo segundo interpretaría HTML y abriría la puerta a inyecciones de código.

// Crear y colgar
const li = document.createElement("li");
li.textContent = "Hogaza de espelta";
lista.append(li);

// Modificar
li.textContent = "Hogaza de espelta (¡última!)";

// Eliminar
li.remove();

Demo en vivo

  • Pan inicial de la lista

«Haz que al pulsar el botón se añada el producto a la lista de la página, y que cada elemento tenga su botón para quitarlo.»

classList — el interruptor de estilos

La forma correcta de cambiar el aspecto desde JavaScript: en vez de tocar estilos uno a uno, JS pone o quita clases y CSS decide cómo se ve cada una. add, remove y sobre todo toggle (pon/quita del tirón: ideal para abrir menús, marcar favoritos, temas claro/oscuro). Así cada tecnología hace su trabajo: JS decide «cuándo», CSS decide «cómo se ve».

const panel = document.querySelector(".panel");

panel.classList.add("abierto");
panel.classList.remove("abierto");
panel.classList.toggle("abierto");   // pon/quita
panel.classList.contains("abierto"); // ¿la tiene?

Demo en vivo

Mi aspecto depende de mi clase

«Haz que el botón encienda y apague el modo destacado de la tarjeta alternando una clase, con el estilo definido en CSS.»

Eventos

addEventListener — escuchar lo que pasa

Un evento es cualquier cosa que ocurre: un clic, una tecla, el envío de un formulario. addEventListener pone un timbre en el elemento: «cuando pase esto, ejecuta esta función». La función receptora recibe el objeto event con todos los detalles (qué elemento fue, qué tecla, dónde estaba el ratón). Es el corazón de toda interactividad.

const boton = document.querySelector("#saludar");

boton.addEventListener("click", (event) => {
  console.log("¡Clic recibido!");
  console.log(event.target); // el elemento pulsado
});

Demo en vivo


      

«Cuando el usuario pulse el botón de añadir al carrito, ejecuta la función que suma el producto y muestra la confirmación.»

Delegación de eventos — un portero para todo el edificio

En vez de poner un timbre en cada piso (un listener por botón), pones un portero en la entrada: un único listener en el contenedor padre que, mirando event.target, sabe qué hijo fue pulsado. Dos ventajas enormes: funciona automáticamente con elementos añadidos después, y consume menos memoria en listas largas. Esta guía la usa para todos sus botones de copiar.

const lista = document.querySelector("#lista-panes");

// UN listener para todos los botones, presentes y futuros
lista.addEventListener("click", (event) => {
  const boton = event.target.closest("button");
  if (!boton) return;              // el clic no fue en un botón
  console.log("Elegiste:", boton.textContent);
});

Demo en vivo


      

«Gestiona los clics de toda la lista con un solo escuchador en el contenedor, de forma que los elementos que se añadan luego funcionen sin código extra.»

Eventos de formulario: submit, input, change, focus, blur

Cinco momentos clave en la vida de un formulario: submit salta al enviar (con event.preventDefault() frenas la recarga y tomas el control); input, con cada letra que se escribe (validación en vivo, contadores); change, al terminar un cambio (elegir en un desplegable, soltar un deslizador); focus y blur, al entrar y salir del campo — blur es el momento educado para validar: cuando el usuario ya terminó de escribir.

formulario.addEventListener("submit", (e) => {
  e.preventDefault();          // frena la recarga
  // validar y enviar con fetch…
});

campo.addEventListener("input", () => { /* cada letra */ });
campo.addEventListener("blur",  () => { /* al salir: validar */ });

Demo en vivo


      

«Valida cada campo cuando el usuario salga de él, actualiza el contador con cada letra y toma el control del envío para mandarlo sin recargar la página.»

Eventos de teclado: keydown y event.key

keydown avisa de cada tecla pulsada y event.key dice cuál fue («Enter», «Escape», «ArrowDown», «a»…). Con ellos se construyen atajos de teclado y componentes navegables sin ratón: cerrar ventanas con Escape, moverse por sugerencias con las flechas. Regla de accesibilidad: todo lo que se pueda hacer con ratón debe poderse hacer con teclado.

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") cerrarModal();
  if (e.key === "ArrowDown") seleccionarSiguiente();
});

Demo en vivo


      

«Haz que la ventana se cierre con la tecla Escape y que por las sugerencias del buscador se pueda navegar con las flechas y elegir con Enter.»

Formularios con JavaScript

Validación personalizada en tiempo real

La validación nativa funciona, pero sus globos son genéricos y desaparecen. La mejora profesional: escuchar blur e input, comprobar el campo con la API de validación (campo.validity dice exactamente qué falla: valueMissing, typeMismatch, tooShort…) y escribir un mensaje propio, claro y en tu idioma, debajo del campo. El mensaje llega por aria-describedby al lector de pantalla y el envío se frena con checkValidity() si algo sigue mal. La demo valida al salir del campo y reevalúa con cada letra:

campo.addEventListener("blur", () => {
  const err = campo.parentElement.querySelector(".error");
  if (campo.validity.valueMissing) {
    err.textContent = "Este campo es obligatorio.";
  } else if (campo.validity.typeMismatch) {
    err.textContent = "Ese correo no tiene buena pinta.";
  } else {
    err.textContent = "";        // todo correcto
  }
  campo.setAttribute("aria-invalid", !campo.checkValidity());
});

Demo en vivo

«Valida los campos en tiempo real con mensajes propios en español debajo de cada uno: al salir del campo si está mal, y quitando el error en cuanto lo corrija.»

FormData — recoger todo el formulario de una vez

En lugar de leer los campos uno a uno, new FormData(formulario) recoge todos los valores de golpe, como vaciar el buzón entero: cada campo con name aporta su valor, los checkboxes marcados aparecen (los desmarcados no), los select múltiples y archivos también. Con Object.fromEntries() se convierte en un objeto normal listo para enviar como JSON. Por eso el atributo name es sagrado: sin él, el campo no existe para FormData.

formulario.addEventListener("submit", (e) => {
  e.preventDefault();
  const datos = new FormData(formulario);
  const objeto = Object.fromEntries(datos);
  // varios valores con el mismo name (checkboxes):
  const extras = datos.getAll("extras");
  console.log(objeto, extras);
});

Demo en vivo

Extras:


        

«Al enviar, recoge todos los campos del formulario con FormData, conviértelos a JSON y muéstrame por consola lo que se enviaría al servidor.»

Normalizar los datos antes de usarlos

Los usuarios escriben « LUCÍA@Gmail.COM » y tu sistema espera «lucia@gmail.com». Normalizar es pasar el dato por el túnel de lavado antes de validarlo o guardarlo: trim() quita espacios de los extremos, toLowerCase() unifica mayúsculas (correos), replaceAll limpia separadores (teléfonos). Ahorra errores absurdos («este correo ya existe» por una mayúscula) y datos sucios en la base de datos.

const normalizarCorreo = (texto) =>
  texto.trim().toLowerCase();

const normalizarTelefono = (texto) =>
  texto.replaceAll(" ", "").replaceAll("-", "");

normalizarCorreo("  LUCÍA@Gmail.COM ");
// "lucía@gmail.com"
normalizarTelefono("600 11-22-33");
// "600112233"

Demo en vivo


      

«Antes de validar o guardar, limpia los datos: quita espacios sobrantes, pasa los correos a minúsculas y deja los teléfonos solo con números.»

Campos condicionales — el formulario que se adapta

Un buen formulario solo pregunta lo necesario: el campo «dirección de envío» no debe aparecer hasta que se elija «envío a domicilio». Se escucha el change del control que decide, y se muestra u oculta el bloque dependiente — ocultándolo con hidden y desactivando sus campos con disabled para que no viajen ni se validen mientras no tocan. Menos campos visibles = más formularios terminados.

radioEnvio.addEventListener("change", () => {
  const aDomicilio = radioEnvio.value === "domicilio";
  bloqueDireccion.hidden = !aDomicilio;
  campoDireccion.disabled = !aDomicilio;
  campoDireccion.required = aDomicilio;
});

Demo en vivo

¿Cómo quieres recibir el pedido?

«Muestra el campo de dirección solo si eligen envío a domicilio, y que mientras esté oculto no sea obligatorio ni se envíe.»

fetch y envío asíncrono

fetch — hablar con el servidor sin recargar

fetch es el mensajero de la página: lleva datos al servidor y trae la respuesta, todo en segundo plano y sin recargar. Sus cuatro estados son sagrados y el usuario debe ver los cuatro: cargando (botón deshabilitado y aviso «enviando…»), éxito, error (con mensaje comprensible y botón de reintentar) y vacío (cuando no hay resultados). Importante: fetch no considera error un 404 o un 500 — hay que comprobar respuesta.ok a mano. En esta demo el servidor es simulado (funciona sin conexión) y falla a propósito la primera vez para que veas el estado de error y el reintento:

async function enviar(datos) {
  try {
    const respuesta = await fetch("/api/pedidos", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(datos)
    });
    if (!respuesta.ok) throw new Error("HTTP " + respuesta.status);
    return await respuesta.json();       // éxito
  } catch (error) {
    // sin conexión o error del servidor → reintentar
    throw error;
  }
}
// En esta guía el servidor se simula en local con una
// función que devuelve una promesa tras una pausa.

Demo en vivo


      

«Envía el formulario con fetch sin recargar la página, mostrando el estado de carga, el mensaje de éxito y, si falla, un aviso claro con botón de reintentar.»

localStorage: recordar entre visitas

localStorage — la libreta del navegador

Una pequeña memoria persistente dentro del navegador: lo que guardes sigue ahí al cerrar y volver. Perfecta para preferencias (tema, idioma — esta guía guarda ambos así), borradores y carritos. Solo almacena texto, así que los objetos van con JSON.stringify al guardar y JSON.parse al leer. Dos límites importantes: es por navegador y dispositivo (tu móvil no ve lo guardado en el portátil; para eso hace falta un servidor) y no es segura para datos sensibles: nunca contraseñas.

// Guardar
localStorage.setItem("tema", "oscuro");
localStorage.setItem("carrito", JSON.stringify(items));

// Leer (null si no existe)
const tema = localStorage.getItem("tema");
const carrito = JSON.parse(localStorage.getItem("carrito")) ?? [];

// Borrar
localStorage.removeItem("carrito");

Demo en vivo


        

Guarda la nota, recarga la página y vuelve aquí: seguirá guardada. Solo en este navegador y este dispositivo.

«Guarda el tema elegido y el borrador del formulario en el navegador, para que al volver a entrar todo siga como lo dejé, y avísame de que solo se guarda en este dispositivo.»

Accesibilidad dinámica

Foco, anuncios y teclado en contenido que cambia

Cuando JavaScript cambia la página, hay que contárselo a quien no la ve: gestión del foco — al abrir un panel, mueve el foco dentro con elemento.focus(); al cerrarlo, devuélvelo al botón de origen (si no, el usuario de teclado queda perdido en el vacío); anuncios — los mensajes que aparecen (errores, «guardado ✓») van en regiones aria-live para que el lector de pantalla los lea solo; y teclado — todo componente creado con JS (modal, menú, pestañas) debe poder abrirse, recorrerse y cerrarse sin ratón (Escape para cerrar es el mínimo). La demo enseña la gestión del foco:

function abrirPanel() {
  panel.hidden = false;
  panel.querySelector("input").focus();   // foco dentro
}
function cerrarPanel() {
  panel.hidden = true;
  botonAbrir.focus();                     // foco de vuelta
}
document.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && !panel.hidden) cerrarPanel();
});

Demo en vivo

«Cuando se abra la ventana, mueve el foco a su interior y devuélvelo al botón al cerrarla; que se pueda cerrar con Escape y que los mensajes nuevos se anuncien al lector de pantalla.»

6 Demostraciones completas

Ocho formularios reales, funcionales de principio a fin: labels visibles, validación nativa reforzada con JavaScript, errores accesibles y estados de carga, éxito y error con envío simulado (nada sale de tu navegador). Cada uno trae su código desplegable y su frase 🗣. La casilla «Simular fallo del servidor» te deja provocar el estado de error a voluntad.

Iniciar sesión

El clásico: correo y contraseña, con botón para mostrar la contraseña y opción de recordar sesión.

Ver el código de este formulario

«Créame un formulario de inicio de sesión con correo y contraseña, botón para ver la contraseña, casilla de recordar sesión, validación en tiempo real y estados de carga, éxito y error al enviar.»

Registro

Alta de usuario completa: confirmación de contraseña con comprobación de coincidencia, medidor de fuerza en vivo y casilla legal obligatoria.

Mínimo 8 caracteres. Mejora con mayúsculas, números y símbolos.

Ver el código de este formulario

«Hazme un formulario de registro accesible con nombre, correo, fecha de nacimiento y contraseña repetida dos veces que compruebe que coinciden, con un medidor de fuerza de la contraseña y la casilla obligatoria de aceptar condiciones.»

Contacto

Formulario de contacto con asunto desplegable, prioridad, mensaje con contador de caracteres y doble validación (nativa + JavaScript).

Prioridad

Entre 20 y 800 caracteres. 0/800

Ver el código de este formulario

«Necesito un formulario de contacto con nombre, correo, asunto desplegable, prioridad y mensaje con contador de caracteres, validado dos veces: por el navegador y por JavaScript con mensajes claros bajo cada campo.»

Búsqueda con autocompletado

Buscador con sugerencias en vivo, navegables con las flechas del teclado y selección con Enter. Busca términos de esta guía: prueba con «flex», «input» o «evento».

Aún no has buscado nada. Escribe arriba y elige una sugerencia.

Ver el código de este formulario

«Añade un buscador que muestre sugerencias mientras escribo, que se puedan recorrer con las flechas del teclado y elegir con Enter, con mensaje claro cuando no haya resultados.»

Encuesta de satisfacción

Radios, checkboxes y un deslizador con su valor en vivo: los tres controles de opinión en un solo formulario.

¿Cómo valoras tu última compra?

¿Qué valoras más? (elige lo que quieras)
7

Ver el código de este formulario

«Monta una encuesta con una valoración de opción única, varios aspectos marcables a la vez, una nota del 0 al 10 con deslizador que muestre el número elegido y un comentario opcional.»

Checkout — finalizar compra

Resumen del carrito con cantidades editables y total recalculado en vivo, datos de envío y forma de pago. Demo educativa: no escribas datos reales de tarjeta.

1 · Tu carrito

Productos del carrito
ProductoPrecioCantidadSubtotal
Hogaza de masa madre 3,50 € 7,00 €
Baguette artesana 1,20 € 1,20 €
Total8,20 €

2 · Datos de envío

3 · Pago

Forma de pago

Ver el código de este formulario

«Créame una página de finalizar compra con el resumen del carrito y el total recalculándose al cambiar cantidades, datos de envío validados, y el campo de tarjeta apareciendo solo si se elige pagar con tarjeta, todo con envío simulado.»

Subida de archivos con vista previa

Adjunta imágenes (o arrástralas a la zona), míralas en miniatura antes de enviar y quita las que sobren. Con límite de tamaño y validación de tipo.

Arrastra imágenes aquí o usa el botón

    Ver el código de este formulario

    «Quiero subir varias imágenes con vista previa en miniatura antes de enviar, pudiendo arrastrarlas o elegirlas con botón, con límite de tamaño y un botón para quitar cada una.»

    Formulario multi-paso

    Tres pasos con barra de progreso, navegación adelante y atrás, validación por paso y resumen final antes de enviar. Los formularios largos convierten mejor troceados.

    1. Tus datos
    2. Tu pedido
    3. Confirmar
    Paso 1 · Tus datos

    Ver el código de este formulario

    «Divide el formulario en tres pasos con barra de progreso, botones de siguiente y atrás, validación antes de avanzar de paso y un resumen final editable antes de enviar.»

    7 Catálogo de prompts listos para copiar

    Peticiones completas en lenguaje natural, tal como se dicen hablando. Cada una indica qué pedir de estructura (HTML), de aspecto (CSS) y de comportamiento (JS). Copia, pega en tu conversación con la IA y ajusta los detalles entre corchetes.

    Registro accesible

    Créame un formulario de registro accesible. De estructura: campos de nombre, correo, contraseña repetida dos veces y casilla de aceptar condiciones, cada uno con su etiqueta visible y agrupados con sentido. De aspecto: diseño limpio a una columna, foco bien visible, botones grandes y estados de error y éxito claramente diferenciados con color e icono. De comportamiento: validación en tiempo real con mensajes en español bajo cada campo, comprobación de que las contraseñas coinciden, medidor de fuerza de la contraseña y envío sin recargar con estados de carga, éxito y error.

    Contacto con doble validación

    Necesito un formulario de contacto para [mi negocio]. De estructura: nombre, correo, asunto en desplegable y mensaje con límite de 800 caracteres, todos con etiquetas visibles y las reglas de obligatorio puestas en el propio HTML. De aspecto: coherente con mi web, campos cómodos en el móvil y contador de caracteres visible. De comportamiento: que JavaScript refuerce la validación nativa con mensajes propios al salir de cada campo, que el envío sea sin recargar y muestre «enviando…», confirmación de éxito y aviso de error con botón de reintentar.

    Checkout con carrito

    Quiero una página de finalizar compra. De estructura: tabla resumen del carrito con producto, precio, cantidad y subtotal; datos de envío completos con autocompletado del navegador; y formas de pago como opciones excluyentes. De aspecto: el total bien destacado, pasos numerados visualmente y todo usable con una mano en el móvil. De comportamiento: recalcular el total al cambiar cantidades, mostrar el campo de tarjeta solo si se elige tarjeta, validar el código postal, y simular el envío con sus estados de carga, éxito y error mientras no haya servidor real.

    Formulario multi-paso

    Divide mi formulario largo en tres pasos: [datos personales], [detalles del pedido] y confirmación. De estructura: cada paso en su propio grupo con título, y un resumen final con todos los datos antes de enviar. De aspecto: barra de progreso arriba que muestre en qué paso estoy y cuánto queda, con los pasos completados marcados. De comportamiento: no dejar avanzar si el paso actual tiene errores, permitir volver atrás sin perder lo escrito, llevar el foco al principio de cada paso nuevo y enviar solo desde el último con confirmación.

    Login elegante

    Hazme una pantalla de inicio de sesión elegante y sobria. De estructura: correo y contraseña con etiquetas visibles, casilla de recordar sesión y enlace de contraseña olvidada. De aspecto: tarjeta centrada con sombra suave, tema claro y oscuro, transiciones sutiles y foco de teclado siempre visible. De comportamiento: botón para mostrar u ocultar la contraseña, validación al salir de cada campo, botón deshabilitado mientras se envía con su indicador de carga, y mensaje de error genérico si las credenciales fallan (sin revelar cuál de las dos está mal).

    Búsqueda con autocompletado

    Añádeme un buscador con autocompletado para [mis productos]. De estructura: campo de búsqueda con su etiqueta, lista de sugerencias correctamente anunciada a los lectores de pantalla y zona de resultados. De aspecto: sugerencias desplegadas justo bajo el campo, con la opción activa resaltada y tamaño cómodo para el dedo. De comportamiento: filtrar sugerencias con cada letra, poder recorrerlas con las flechas y elegir con Enter o con un toque, cerrar con Escape, y mostrar un mensaje amable cuando no haya coincidencias.

    Encuesta

    Créame una encuesta de satisfacción. De estructura: una valoración de opción única con radios agrupados bajo su pregunta, varios aspectos marcables con checkboxes, una nota del 0 al 10 con deslizador y un comentario opcional. De aspecto: opciones grandes y fáciles de tocar, el valor del deslizador siempre visible y una barra de progreso si la encuesta es larga. De comportamiento: exigir solo la valoración obligatoria, mostrar el número del deslizador en vivo y, al enviar, agradecer la participación con un resumen de lo respondido.

    Landing completa

    Quiero una landing completa para [producto o servicio]. De estructura: cabecera con navegación, sección principal con titular claro y botón de acción, tres beneficios con iconos, testimonios, preguntas frecuentes desplegables sin JavaScript y pie con contacto. De aspecto: paleta sobria de un solo color de acento, tipografía del sistema, mucho espacio en blanco, tema claro y oscuro y diseño móvil primero. De comportamiento: menú hamburguesa accesible en móvil, desplazamiento suave a las secciones y el formulario de la parte final validado y con envío simulado.

    Componente: tarjeta de producto

    Diséñame una tarjeta de producto reutilizable. De estructura: imagen con su texto alternativo, nombre, precio, etiqueta opcional de oferta y botón de añadir al carrito, todo como una pieza autónoma. De aspecto: esquinas redondeadas, sombra sutil que crece un poco al pasar el ratón, precio destacado y versión coherente en tema oscuro. De comportamiento: al añadir al carrito, confirmación visual en la propia tarjeta sin recargar, contador de unidades y aviso accesible para lectores de pantalla. Dame también un ejemplo con seis tarjetas en una cuadrícula adaptable.

    Componente: modal accesible

    Créame una ventana modal accesible para [confirmar una acción]. De estructura: usa el elemento dialog nativo, con título, texto y botones de confirmar y cancelar. De aspecto: fondo oscurecido tras la ventana, aparición con fundido suave que respete el movimiento reducido y botones con buen tamaño táctil. De comportamiento: al abrirse, el foco entra a la ventana y no puede escaparse de ella con Tab; se cierra con Escape, con el botón de cancelar o pulsando fuera; y al cerrarse, el foco vuelve exactamente al botón que la abrió.

    8 Tabla de referencia rápida

    Todas las capacidades importantes en una sola tabla filtrable: qué es cada recurso, para qué sirve, cómo pedírselo a la IA hablando, un ejemplo mínimo y sus advertencias. Escribe para filtrar o limita por categoría.

    Referencia rápida de HTML, CSS y JavaScript
    Recurso Para qué sirve Cómo pedírselo a la IA Ejemplo Advertencias

    9 Buenas prácticas y qué evitar

    El resumen del oficio: lo que separa una página correcta de una profesional. Si pides estas cosas a la IA (o las repasas en su resultado), el salto de calidad es inmediato.

    Hazlo así

    Estructura con sentido

    Etiquetas semánticas para cada zona, un único <h1>, jerarquía de títulos sin saltos y el contenido importante en el HTML (no inyectado todo por JS). El documento debe entenderse leyendo solo las etiquetas.

    Accesibilidad desde el diseño

    Labels visibles y asociadas, contraste suficiente, foco visible, tamaños táctiles de 44 px, navegación completa por teclado y ARIA solo donde HTML no llega. No es una capa final: se pide desde el primer mensaje.

    Validación en tres capas

    HTML pone las reglas (required, type, pattern), JavaScript mejora los mensajes y la experiencia, y el servidor revalida siempre. Cada capa cubre el fallo de la anterior.

    Móvil primero

    Diseña para la pantalla pequeña y mejora hacia arriba con media queries. Unidades relativas (rem, %) para que todo escale, y párrafos limitados a un ancho cómodo de lectura.

    Feedback en los cuatro estados

    Toda acción muestra su estado: vacío, cargando, éxito y error. Botones deshabilitados mientras se envía, mensajes con color e icono, y anuncios en regiones aria-live para lectores de pantalla.

    Separación de responsabilidades

    HTML dice qué es, CSS cómo se ve, JS cómo se comporta — en archivos separados. JS cambia clases, no estilos sueltos; CSS centraliza su sistema en variables. Así cada cambio se hace en un solo sitio.

    Código que se puede mantener

    Nombres descriptivos, funciones pequeñas con una sola tarea, configuración agrupada al principio, comentarios que explican el porqué (no el qué) y cero variables globales sueltas.

    Rendimiento y respeto

    Imágenes con dimensiones declaradas (sin saltos de página), animaciones solo de transform y opacity, respeto a prefers-reduced-motion y prefers-color-scheme, y nada de dependencias que no aporten.

    Antipatrones: si lo ves, corrígelo

    Elementos obsoletos (solo para reconocerlos)

    Los verás en páginas antiguas y en malos tutoriales. No se usan nunca en código nuevo; cada uno tiene su sustituto moderno: