Tres componentes: ventana modal, carrusel y buscador
Una galería que se abre en grande, un carrusel de testimonios y un buscador que filtra mientras escribes aparecen en casi todos los portfolios.
Modal: <dialog> + showModal() y close(); Escape y el foco, gratis.
Carrusel: un array, un índice y % para dar la vuelta; aria-live para anunciar el cambio.
Buscador: evento input, toLowerCase() + includes() y hidden en lo que no coincide.
- Una galería que se abre en grande, un carrusel de testimonios y un buscador que filtra mientras escribes aparecen en casi todos los portfolios. Si se los pides a una IA te los hace en segundos, pero para saber si lo que te da está bien tienes que conocer cómo se construyen. Aquí tienes los tres, hechos con lo que ya sabes: querySelector, eventos, clases, atributos y arrays.
- Una ventana modal se pone encima de la página y no deja usar lo de debajo hasta que se cierra. HTML tiene un elemento para eso, <dialog>, que ya resuelve lo difícil: se cierra con la tecla Escape, mantiene el foco dentro mientras está abierta y lo devuelve al botón que la abrió al cerrarse.
- <ul class="miniaturas"> <li> <button type="button" class="miniatura" data-grande="img/cartel-1.webp" data-alt="Cartel del Festival Sur, tipografía roja sobre fondo crema"> <img src="img/cartel-1-mini.webp" alt="Cartel del Festival Sur"> </button> </li> <!-- … más miniaturas … --> </ul> <dialog id="visor" aria-label="Imagen ampliada"> <img id="visor-imagen" src="" alt=""> <button type="button" id="cerrar">Cerrar</button> </dialog>
- const visor = document.querySelector("#visor"); const imagen = document.querySelector("#visor-imagen"); document.querySelectorAll(".miniatura").forEach((boton) => { boton.addEventListener("click", () => { imagen.src = boton.dataset.grande; // la imagen grande, de data-grande imagen.alt = boton.dataset.alt; visor.showModal(); // abre como modal: encima de todo }); }); document.querySelector("#cerrar").addEventListener("click", () => visor.close()); // Cerrar también al pulsar fuera de la imagen (en el fondo oscuro) visor.addEventListener("click", (evento) => { if (evento.target === visor) visor.close(); });
- dialog { border: 0; padding: 0; max-width: min(90vw, 60rem); } dialog img { display: block; width: 100%; height: auto; } dialog::backdrop { background: rgb(0 0 0 / 0.75); } /* el fondo de detrás */
Introducción
Una galería que se abre en grande, un carrusel de testimonios y un buscador que filtra mientras escribes aparecen en casi todos los portfolios. Si se los pides a una IA te los hace en segundos, pero para saber si lo que te da está bien tienes que conocer cómo se construyen. Aquí tienes los tres, hechos con lo que ya sabes: querySelector, eventos, clases, atributos y arrays.
Ventana modal con <dialog>
Una ventana modal se pone encima de la página y no deja usar lo de debajo hasta que se cierra. HTML tiene un elemento para eso, <dialog>, que ya resuelve lo difícil: se cierra con la tecla Escape, mantiene el foco dentro mientras está abierta y lo devuelve al botón que la abrió al cerrarse.
<ul class="miniaturas"> <li> <button type="button" class="miniatura" data-grande="img/cartel-1.webp" data-alt="Cartel del Festival Sur, tipografía roja sobre fondo crema"> <img src="img/cartel-1-mini.webp" alt="Cartel del Festival Sur"> </button> </li> <!-- … más miniaturas … --> </ul> <dialog id="visor" aria-label="Imagen ampliada"> <img id="visor-imagen" src="" alt=""> <button type="button" id="cerrar">Cerrar</button> </dialog>
const visor = document.querySelector("#visor"); const imagen = document.querySelector("#visor-imagen"); document.querySelectorAll(".miniatura").forEach((boton) => { boton.addEventListener("click", () => { imagen.src = boton.dataset.grande; // la imagen grande, de data-grande imagen.alt = boton.dataset.alt; visor.showModal(); // abre como modal: encima de todo }); }); document.querySelector("#cerrar").addEventListener("click", () => visor.close()); // Cerrar también al pulsar fuera de la imagen (en el fondo oscuro) visor.addEventListener("click", (evento) => { if (evento.target === visor) visor.close(); });
dialog { border: 0; padding: 0; max-width: min(90vw, 60rem); } dialog img { display: block; width: 100%; height: auto; } dialog::backdrop { background: rgb(0 0 0 / 0.75); } /* el fondo de detrás */
Carrusel de testimonios
Un carrusel muestra un elemento de una lista cada vez y tiene botones para pasar al anterior y al siguiente. Por dentro es un array y un número, el índice del que se ve:
<section class="carrusel" aria-label="Testimonios"> <blockquote id="testimonio" aria-live="polite"></blockquote> <button type="button" id="anterior" aria-label="Testimonio anterior">←</button> <button type="button" id="siguiente" aria-label="Testimonio siguiente">→</button> </section>
const testimonios = [ { texto: "Entendió la marca a la primera.", autor: "Café Lento" }, { texto: "Entregó antes de plazo y sin sustos.", autor: "Librería Sur" }, { texto: "El cartel se agotó en una semana.", autor: "Festival Norte" }, ]; let indice = 0; const caja = document.querySelector("#testimonio"); function mostrar() { const t = testimonios[indice]; caja.innerHTML = ""; const p = document.createElement("p"); p.textContent = t.texto; const pie = document.createElement("footer"); pie.textContent = `— ${t.autor}`; caja.append(p, pie); } document.querySelector("#siguiente").addEventListener("click", () => { indice = (indice + 1) % testimonios.length; // del último vuelve al primero mostrar(); }); document.querySelector("#anterior").addEventListener("click", () => { indice = (indice - 1 + testimonios.length) % testimonios.length; // del primero va al último mostrar(); }); mostrar();
El % hace que el índice dé la vuelta, igual que el tono en el círculo cromático (lección Datos, variables y operadores): sumar la longitud antes de restar evita los negativos. aria-live="polite" hace que el lector de pantalla lea el testimonio nuevo cuando cambia. Los botones solo tienen una flecha, así que llevan aria-label. Si pasa solo cada pocos segundos, añade un botón de pausa y no lo muevas con prefers-reduced-motion: reduce (lección Variables, tipografía web y animación): un contenido que se mueve sin que lo pidas molesta y es un problema de accesibilidad.
Buscador en vivo
Filtra una lista mientras escribes: con cada tecla (evento input, lección Formularios con JavaScript) compara el texto con el título de cada tarjeta y oculta las que no coinciden.
<label for="buscador">Buscar proyectos</label> <input id="buscador" type="search"> <p id="resultados" aria-live="polite"></p> <article class="tarjeta"><h3>Cartel Festival Sur</h3>…</article> <article class="tarjeta"><h3>Identidad Café Lento</h3>…</article>
const buscador = document.querySelector("#buscador"); const tarjetas = document.querySelectorAll(".tarjeta"); const resultados = document.querySelector("#resultados"); buscador.addEventListener("input", () => { const texto = buscador.value.trim().toLowerCase(); let visibles = 0; tarjetas.forEach((tarjeta) => { const titulo = tarjeta.querySelector("h3").textContent.toLowerCase(); const coincide = titulo.includes(texto); tarjeta.hidden = !coincide; // oculta las que no coinciden if (coincide) visibles++; }); resultados.textContent = visibles === 0 ? "No hay proyectos con ese nombre." : `${visibles} proyectos`; });
toLowerCase() en los dos lados: así «cartel» encuentra «Cartel». trim() quita los espacios del principio y del final. Con el campo vacío, includes("") es true para todos: se ven todas las tarjetas. El párrafo con aria-live cuenta los resultados a quien no ve la pantalla.
Qué mirar en el código que te dé una IA
Con estos tres modelos delante, revisa lo que te genere con la lista de comprobación de la lección Programar con un copiloto de IA. Lo que más suele fallar en estos componentes:
Divs o imágenes con onclick en lugar de <button>: no funcionan con el teclado. Una modal hecha con div que no se cierra con Escape ni devuelve el foco. Un carrusel que se sale del array (índice -1 o igual a la longitud) por no usar %. Un buscador que distingue mayúsculas, o que no dice nada cuando no hay resultados. Librerías enteras (jQuery, un plugin de carrusel) para algo que son veinte líneas.
Errores frecuentes
Llamar a visor.show() en lugar de showModal(): se abre, pero no como modal (no hay fondo ni se cierra con Escape). Olvidar el alt de la imagen grande al cambiar su src. Usar innerHTML con texto que escribe el usuario: usa textContent. En el buscador, escuchar change en lugar de input: solo filtra al salir del campo. Poner hidden a una tarjeta que tiene display: flex o grid en el CSS y que no desaparezca: tu regla gana a la del navegador. Añade [hidden] { display: none !important; } a tu hoja de estilos o usa una clase .oculto.