Maquetación Web Dinámica / JavaScript para maquetadores: darle vida al DOM
Patrones de UI: modal, tabs, acordeón y dark mode
Hay un puñado de componentes que te van a pedir en CADA proyecto: un modal, pestañas, un acordeón, un interruptor de tema oscuro. Son tan comunes que conviene tenerlos como recetas mentales, hechos bien —accesibles— de una vez. Esta lección es tu recetario: los patrones clásicos con todo lo que aprendiste en el módulo, y con la accesibilidad que separa un componente profesional de uno a medias.
El reflector: un modal que se comporta
<button id="abrir" class="btn">Abrir modal</button>
<div id="modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="titulo" aria-hidden="true">
<div class="dialogo">
<h2 id="titulo">¿Confirmas la acción?</h2>
<p>Este modal cierra con el botón, con la tecla Escape o tocando el fondo oscuro.</p>
<button id="cerrar" class="btn">Entendido</button>
</div>
</div>body { font-family: system-ui; padding: 20px; background: #f8fafc; }
.btn { padding: 10px 18px; border: 0; border-radius: 8px; background: #6d28d9; color: white; font-weight: 600; font-size: 14px; cursor: pointer; }
.btn:focus-visible { outline: 3px solid #a78bfa; outline-offset: 2px; }
.modal { position: fixed; inset: 0; display: grid; place-items: center; background: rgba(15,23,42,0.6); opacity: 0; visibility: hidden; transition: opacity 200ms ease; padding: 16px; }
.modal.abierto { opacity: 1; visibility: visible; }
.dialogo { background: white; border-radius: 14px; padding: 24px; max-width: 340px; display: grid; gap: 12px; }
.dialogo h2 { margin: 0; font-size: 19px; color: #1e293b; }
.dialogo p { margin: 0; font-size: 14px; color: #475569; line-height: 1.6; }
@media (prefers-reduced-motion: reduce) { .modal { transition: none; } }const abrir = document.querySelector("#abrir");
const modal = document.querySelector("#modal");
const cerrar = document.querySelector("#cerrar");
let previo = null; // recordar quién tenía el foco antes
function abrirModal() {
previo = document.activeElement;
modal.classList.add("abierto");
modal.setAttribute("aria-hidden", "false");
cerrar.focus(); // llevar el foco DENTRO del modal
}
function cerrarModal() {
modal.classList.remove("abierto");
modal.setAttribute("aria-hidden", "true");
if (previo) previo.focus(); // devolver el foco a donde estaba
}
abrir.addEventListener("click", abrirModal);
cerrar.addEventListener("click", cerrarModal);
// clic en el fondo (no en el diálogo) cierra
modal.addEventListener("click", (event) => {
if (event.target === modal) cerrarModal();
});
// Escape cierra (solo si está abierto)
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal.classList.contains("abierto")) cerrarModal();
});Ábrelo y ciérralo de las tres formas, y prueba con el teclado (Tab,
Escape). Lo que lo hace accesible: el foco ENTRA al modal al abrir
(cerrar.focus()), VUELVE al botón original al cerrar (previo.focus()),
Escape y el clic en el fondo lo cierran, y los aria describen su rol.
(Un modal de producción además ATRAPA el foco en un ciclo dentro del
diálogo; el <dialog> nativo de HTML te da varias de estas gratis y vale
la pena conocerlo.)
Las otras tres recetas
// TABS: un botón por pestaña (role="tab"), un panel por contenido
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
tabs.forEach((t) => t.setAttribute("aria-selected", "false"));
paneles.forEach((p) => (p.hidden = true));
tab.setAttribute("aria-selected", "true"); // marca la activa
document.querySelector(tab.dataset.panel).hidden = false; // muestra su panel
});
});// ACORDEÓN: un botón con aria-expanded que muestra/oculta su contenido
boton.addEventListener("click", () => {
const abierto = boton.getAttribute("aria-expanded") === "true";
boton.setAttribute("aria-expanded", String(!abierto));
contenido.hidden = abierto;
});
// (o el nativo: <details><summary>Título</summary>...</details>, sin JS)
// DARK MODE persistente: la clase + localStorage (persistencia del módulo 7)
const guardado = localStorage.getItem("tema"); // "oscuro" o null
if (guardado === "oscuro") document.body.classList.add("oscuro");
toggle.addEventListener("click", () => {
const oscuro = document.body.classList.toggle("oscuro");
localStorage.setItem("tema", oscuro ? "oscuro" : "claro"); // recuerda la elección
});Fíjate en el hilo común de las cuatro recetas: JavaScript cambia un
estado (una clase o un aria), el CSS pinta, y la accesibilidad se
sincroniza — exactamente la regla de oro de la lección 2, aplicada una y
otra vez. Los patrones cambian; el principio no.
Tu modal se ve perfecto, pero un usuario que navega con teclado se queja: al abrirlo, sigue 'atrapado' navegando la página de atrás, y al cerrarlo no sabe dónde quedó. ¿Qué falta?
Mini-reto
Elige UNA receta y móntala en "El Observador" con accesibilidad completa:
un modal de "suscríbete" (con foco y Escape), o pestañas para las secciones
(Portada / Opinión / Deportes con aria-selected), o el dark mode
persistente. Pruébalo SOLO con teclado, sin tocar el mouse: si puedes
abrir, usar y cerrar todo con Tab, Enter y Escape, tu componente está a
nivel profesional.
Qué sigue
Tienes el recetario completo. El reto del módulo los combina en una página real: scroll reveals que aparecen al bajar, un navbar inteligente que se transforma, y un modal accesible — los tres patrones que definen una landing moderna, integrados por ti. El examen de "darle vida al DOM".