#Bootstrap 5.3.8 paso a paso: construye el sitio de tu club de videojuegos
Tutorial guiado de autoestudio · 3 horas y 30 minutos · Nivel principiante
🧪 Versión 2 (revisada y verificada). Corrige cuatro problemas reales que tenía la v1 y que se comprobaron abriendo el sitio en un navegador:
Problema Qué hace ahora El sitio se desplazaba de lado a lado en celular La portada usa g-4 g-lg-5y el CSS parte los textos largos conoverflow-wrapEl modo oscuro ignoraba la preferencia del sistema Arranca con el tema del dispositivo ( prefers-color-scheme) y recuerda el que elijasLa página parpadeaba al cargar Un script en el <head>aplica el tema antes de pintarAl enviar el formulario el foco no se movía El foco salta al primer campo con error y al mensaje de éxito Además: la galería enseña a usar imágenes reales con
alt, y se aclara cuándo usar Sass.
¡Bienvenido/a! En las próximas 3 horas y media vas a construir PixelCrew, la página web de un club de videojuegos de colegio. Vas a empezar con un archivo casi vacío y terminar con un sitio completo, responsive, con modo oscuro y modo claro, formulario con validación, carrusel, acordeón y ventana emergente.
Y lo más importante: al final no solo tendrás un sitio terminado. Tendrás las herramientas para crear tus propios proyectos con Bootstrap sin necesitar este tutorial.
#Tabla de contenidos
- Antes de empezar
- Módulo 1: Preparación del entorno (15 min)
- Módulo 2: Contenedores, grid y breakpoints (30 min)
- Módulo 3: Tipografía, color y utilidades (20 min)
- Módulo 4: Navbar y navegación responsive (25 min)
- Módulo 5: Tarjetas, botones, badges y alertas (25 min)
- Módulo 6: Formularios y validación (25 min)
- Módulo 7: Componentes interactivos con JavaScript (25 min)
- Módulo 8: Personalización, modo oscuro y Sass (20 min)
- Módulo 9: Integración final y buenas prácticas (15 min)
- Módulo 10: Tu reto final y siguientes pasos (10 min)
- Código completo del proyecto
- Hoja de referencia rápida
- Retos adicionales y recursos oficiales
#Antes de empezar
#Qué vas a construir
Una página con estas secciones, todas hechas con Bootstrap 5.3.8:
| Sección | Lo que practicas |
|---|---|
| Barra de navegación | Navbar responsive con menú hamburguesa |
| Hero (portada) | Tipografía, utilidades, color |
| Estadísticas y galería | Sistema de grid y breakpoints |
| "Qué hacemos" | Tarjetas, botones, badges, alertas |
| Comunidad | Carrusel |
| Preguntas frecuentes | Acordeón |
| Inscripción | Formularios con validación |
| Reglas del club | Modal (ventana emergente) |
| Todo el sitio | Modo claro/oscuro y personalización |
#Prerrequisitos
- Saber lo básico de HTML (etiquetas, atributos, enlaces) y de CSS (selectores, colores, márgenes).
- Un editor de código. Recomendado: Visual Studio Code con la extensión Live Server (te recarga la página sola cada vez que guardas).
- Un navegador moderno (Chrome, Edge o Firefox).
- Conexión a internet mientras practicas (Bootstrap se carga desde un CDN).
- No necesitas instalar nada más, ni saber JavaScript, ni conocer Bootstrap.
#Cómo usar este tutorial
- Hazlo en orden. Cada módulo agrega una pieza al mismo sitio, así que los módulos dependen unos de otros.
- Escribe o pega el código tú mismo/a y míralo funcionar. No basta con leer.
- Cada módulo tiene el mismo formato: objetivo, concepto, pasos, resultado esperado, errores comunes, mini-ejercicio y punto de control.
- Intenta el mini-ejercicio antes de abrir la pista y la solución. Equivocarse ahí es parte del aprendizaje.
- Usa el punto de control al final de cada módulo. Si no puedes marcar todo, repasa antes de seguir.
- Si algo no se ve como debería, revisa la sección de errores comunes y las Herramientas del navegador (tecla
F12).
Íconos que verás: 💡 tip · ⚠️ ojo con esto · ✅ punto de control.
#Plan de tiempo
| Módulo | Tema | Tiempo |
|---|---|---|
| 1 | Preparación del entorno | 15 min |
| 2 | Contenedores, grid y breakpoints | 30 min |
| 3 | Tipografía, color y utilidades | 20 min |
| 4 | Navbar y navegación responsive | 25 min |
| 5 | Tarjetas, botones, badges y alertas | 25 min |
| 6 | Formularios y validación | 25 min |
| 7 | Componentes interactivos (JS) | 25 min |
| 8 | Personalización, modo oscuro y Sass | 20 min |
| 9 | Integración final y buenas prácticas | 15 min |
| 10 | Reto final: planificación y primer paso | 10 min |
| Total | 210 min (3 h 30 min) |
💡 Los tiempos son estimaciones para una persona que escribe a ritmo normal. Si vas más rápido, aprovecha para hacer los retos adicionales; si vas más lento, está bien: lo importante es entender, no terminar primero.
#Suposiciones que hice
Para no dejar decisiones abiertas, asumí lo siguiente:
- Tema del proyecto: un club de videojuegos de colegio (PixelCrew). Los nombres, cifras y testimonios son ficticios.
- Imágenes: para que no dependas de descargar nada, la galería empieza con emojis sobre bloques de color. En el módulo 2 verás cómo cambiarlos por fotos reales con su
alt(usaremos fotos de ejemplo de internet; en tu club, pon las tuyas). - Modo claro u oscuro: el sitio arranca con el tema que ya tiene tu dispositivo (
prefers-color-scheme) y recuerda el que elijas después. - Bootstrap por CDN, con la versión exacta 5.3.8 y sus hashes de integridad. Sass se presenta como lectura opcional: no hace falta para nada de este tutorial.
- El formulario no envía datos a ningún servidor: es solo práctica de interfaz y validación.
- Todo el contenido está en español y usa solo clases de Bootstrap 5 (nada de jQuery ni de clases de Bootstrap 4).
#Módulo 1: Preparación del entorno
⏱ 15 minutos
🎯 Objetivo: crear la estructura de archivos, cargar Bootstrap 5.3.8 y comprobar que funciona.
#Concepto
Bootstrap es un framework de front-end: una colección de CSS y JavaScript ya escrita que te da tres cosas:
- Un sistema de rejilla (grid) para organizar la página en filas y columnas.
- Componentes listos para usar: barras de navegación, tarjetas, botones, formularios, modales...
- Utilidades: clases pequeñas que hacen una sola cosa (
mt-3= margen arriba,text-center= texto centrado).
Es mobile-first: el diseño base piensa en celulares y luego se adapta a pantallas más grandes.
Hay dos formas de usarlo. Usaremos la primera:
| Forma | Cómo | Cuándo |
|---|---|---|
| CDN | Enlazas el CSS y el JS desde internet | Aprender, prototipos, sitios sencillos |
| npm + Sass | Lo instalas y lo compilas | Proyectos grandes con personalización profunda |
Bootstrap tiene dos archivos: el CSS (los estilos) y el bundle JS, que incluye todo el JavaScript de los componentes y a Popper (necesario para menús desplegables). Por eso usamos bootstrap.bundle.min.js y no el JS suelto.
#Pasos
1. Crea esta estructura de carpetas y archivos (puedes hacerlo desde VS Code):
pixelcrew/
├── index.html
├── css/
│ └── style.css
└── js/
└── main.js
Deja style.css y main.js vacíos por ahora.
2. Pega esta plantilla en index.html. Es el esqueleto de todo el proyecto. Fíjate en los comentarios <!-- HERO -->, <!-- NAVBAR -->, etc.: son marcadores donde pegarás código en los módulos siguientes.
<!DOCTYPE html>
<html lang="es" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="PixelCrew: el club de videojuegos de tu colegio. Torneos, noches de juego y talleres.">
<title>PixelCrew | Club de videojuegos</title>
<!-- Aplica el tema ANTES de que se vea la página (evita el parpadeo).
Va primero en el <head> a propósito; en un sitio real se le llama
"anti-flash" y casi siempre se escribe en una sola línea. -->
<script>
(function () {
try {
var guardado = localStorage.getItem('pc-tema');
var oscuroDelSistema = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-bs-theme', guardado || (oscuroDelSistema ? 'dark' : 'light'));
} catch (error) {
document.documentElement.setAttribute('data-bs-theme', 'light');
}
})();
</script>
<!-- Bootstrap 5.3.8 (CSS) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<!-- Tu CSS (siempre DESPUÉS de Bootstrap) -->
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- NAVBAR -->
<main id="contenido">
<!-- PRUEBA -->
<!-- HERO -->
<!-- ESTADÍSTICAS -->
<!-- QUÉ HACEMOS -->
<!-- GALERÍA -->
<!-- COMUNIDAD -->
<!-- PREGUNTAS -->
<!-- ÚNETE -->
</main>
<!-- FOOTER -->
<!-- MODAL -->
<!-- Bootstrap 5.3.8 (JS con Popper incluido) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<!-- Tu JavaScript (después de Bootstrap) -->
<script src="js/main.js"></script>
</body>
</html>
Observa lo importante:
<html lang="es" data-bs-theme="light">: idioma español y el tema inicial.- El script del
<head>aplica el tema guardado (o el de tu dispositivo) antes de que se pinte nada. Sin él, el navegador muestra un tema claro y luego salta a oscuro: eso es el parpadeo. <meta name="viewport" ...>: sin esta línea, Bootstrap no se adapta a celulares.- El CSS de Bootstrap va antes que tu
style.css, para que tus estilos puedan sobrescribirlo. - Los scripts van al final del
<body>. - Los atributos
integrityycrossoriginverifican que el archivo del CDN no fue alterado. No los modifiques.
3. Agrega un bloque de prueba. Pega esto justo debajo del comentario <!-- PRUEBA -->:
<div class="container py-5">
<h1 class="display-4">¡Hola, Bootstrap 5.3.8!</h1>
<p class="lead">Si ves esto con estilo, todo está funcionando.</p>
<button type="button" class="btn btn-primary">Mi primer botón</button>
</div>
4. Ábrelo en el navegador. Con Live Server: clic derecho en index.html → Open with Live Server. Sin Live Server: doble clic en el archivo.
5. Comprueba la versión. Abre las herramientas del navegador (F12), ve a la pestaña Console y escribe:
bootstrap.Tooltip.VERSION
Debe responder "5.3.8".
#Resultado esperado
Una página oscura con un título grande, un párrafo destacado y un botón azul con esquinas redondeadas. Si ves letras blancas simples sobre fondo blanco y un botón gris, Bootstrap no cargó.
#Errores comunes
| Síntoma | Causa probable | Solución |
|---|---|---|
| La página se ve sin estilos | Sin internet, o error al copiar la URL del CDN | Revisa la conexión y compara los enlaces con los de la plantilla |
La consola dice que bloqueó el recurso por integrity |
Se alteró el hash al copiar | Vuelve a copiar la línea completa, sin espacios extra |
| El botón se ve bien pero los componentes no responden | Falta el <script> del bundle |
Revisa que esté antes de </body> |
| En el celular todo se ve diminuto | Falta la etiqueta viewport |
Agrégala dentro de <head> |
bootstrap is not defined en la consola |
El script no cargó | Revisa la URL y la pestaña Network (F12) |
| La página parpadea blanco al cargar | Borraste el script del <head> o lo moviste después del CSS |
Ponlo de nuevo, lo más arriba posible en el <head> |
| El modo oscuro no aparece aunque tu sistema esté en oscuro | Quitaste matchMedia('(prefers-color-scheme: dark)') |
Ese matchMedia es lo que hace que el sitio respete tu tema del sistema |
#Mini-ejercicio
- Cambia
data-bs-theme="light"pordata-bs-theme="dark"en la etiqueta<html>y borra por un momento el script del<head>: verás el sitio oscuro, pero también el parpadeo al recargar. Después vuelve a poner el script. - En el sistema operativo, cambia tu tema a oscuro y abre el sitio: ¿qué modo aparece? (Pista:
prefers-color-scheme). - Cambia el botón de
btn-primaryabtn-success,btn-dangerybtn-warning.
💡 Pista
Solo cambian palabras dentro del atributo `class`. Bootstrap tiene una clase para cada color: `btn-primary`, `btn-secondary`, `btn-success`, `btn-danger`, `btn-warning`, `btn-info`, `btn-light`, `btn-dark`.#✅ Punto de control
- [ ] Tengo la estructura
pixelcrew/con sus tres archivos. - [ ] Mi página muestra el título y el botón con estilo de Bootstrap.
- [ ]
bootstrap.Tooltip.VERSIONresponde"5.3.8"en la consola. - [ ] Sé cambiar entre modo claro y oscuro con
data-bs-theme. - [ ] Entiendo para qué sirve el script del
<head>y sé por qué va antes que el CSS.
#Módulo 2: Contenedores, grid y breakpoints
⏱ 30 minutos
🎯 Objetivo: organizar contenido en filas y columnas que se reacomodan según el tamaño de pantalla.
#Concepto
El grid de Bootstrap tiene tres piezas que siempre van anidadas en este orden:
.container → .row → .col-*
(centra y (fila, (columna: cada fila se divide
limita el agrupa en 12 partes)
ancho) columnas)
Cada fila tiene 12 unidades. Una col-6 ocupa la mitad, una col-4 un tercio, una col-3 un cuarto. Si las columnas suman más de 12, las sobrantes saltan a la línea de abajo.
#Breakpoints: el corazón de lo responsive
Un breakpoint es el ancho de pantalla a partir del cual cambia el diseño. Bootstrap tiene seis:
| Prefijo | Se activa desde | Dispositivo típico |
|---|---|---|
| (ninguno) | 0 px | Celular vertical |
sm |
576 px | Celular horizontal |
md |
768 px | Tablet |
lg |
992 px | Laptop |
xl |
1200 px | Escritorio |
xxl |
1400 px | Pantalla grande |
La regla clave es mobile-first: una clase sin prefijo aplica a todas las pantallas, y una clase con prefijo aplica desde ese tamaño hacia arriba. Ejemplo:
<div class="col-12 col-md-6 col-lg-4">
Se lee así: "ocupa las 12 columnas (todo el ancho) en celular; 6 (la mitad) desde tablet; 4 (un tercio) desde laptop".
Otras herramientas del grid que usaremos:
g-3: gutter (espacio entre columnas) de tamaño 3.row-cols-2: en vez de dar un ancho a cada columna, dices cuántas columnas caben por fila.container(ancho fijo por breakpoint) vscontainer-fluid(siempre 100 % del ancho).
#Pasos
1. Laboratorio de grid (temporal). Reemplaza el bloque de prueba del módulo anterior por este (bórralo todo y pega):
<div class="container py-4">
<div class="row g-3">
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-primary-subtle border rounded text-center">A</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-success-subtle border rounded text-center">B</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-danger-subtle border rounded text-center">C</div>
</div>
</div>
</div>
2. Experimenta. Abre las herramientas del navegador (F12) y activa el modo responsive (el ícono de celular y tablet). Cambia el ancho y observa:
- Menos de 768 px → las tres cajas apiladas, una por fila.
- Entre 768 y 991 px → dos por fila (la C baja).
- 992 px o más → tres en una fila.
💡 Entre 768 y 991 px, A y B (col-md-6 cada una) suman las 12 unidades de la fila, por eso C no cabe y salta a la línea de abajo.
3. Borra el laboratorio. Ya aprendiste lo esencial. Elimina todo el bloque que pegaste en el paso 1.
4. Construye la franja de estadísticas. Pega justo debajo de <!-- ESTADÍSTICAS -->:
<section class="py-4 border-top border-bottom bg-body-tertiary" aria-label="El club en números">
<div class="container">
<div class="row g-3 text-center">
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">120+</p>
<p class="text-body-secondary mb-0">miembros</p>
</div>
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">12</p>
<p class="text-body-secondary mb-0">torneos al año</p>
</div>
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">3</p>
<p class="text-body-secondary mb-0">talleres semanales</p>
</div>
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">1</p>
<p class="text-body-secondary mb-0">sala gamer</p>
</div>
</div>
</div>
</section>
Aquí hay varias ideas nuevas:
col-6 col-md-3: dos por fila en celular, cuatro por fila desde tablet.bg-body-tertiary: un fondo que se adapta solo al modo claro u oscuro.border-top border-bottom: bordes arriba y abajo.display-6: un texto grande y llamativo (más sobre esto en el módulo 3).
5. Construye la galería. Pega justo debajo de <!-- GALERÍA -->:
<section id="galeria" class="py-5">
<div class="container">
<h2 class="text-center mb-2">Momentos del club</h2>
<p class="text-center text-body-secondary mb-4">Un vistazo a lo que pasa en las reuniones. (Aquí irían tus fotos reales.)</p>
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-primary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un trofeo">🏆</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-success d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un joystick">🕹️</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-danger d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de unos audífonos">🎧</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-warning d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un computador">💻</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-info d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un dado">🎲</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-secondary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un apretón de manos">🤝</div>
</div>
</div>
</div>
</div>
</section>
Mira la clase row row-cols-2 row-cols-md-3 row-cols-lg-6: ya no le damos ancho a cada columna, sino que decimos cuántas caben: 2 en celular, 3 en tablet, 6 en laptop. Además:
ratio ratio-1x1crea un cuadrado perfecto sin importar el ancho.role="img"yaria-labeldescriben el emoji para lectores de pantalla (accesibilidad).
6. (Opcional) Cambia dos cuadrados por fotos reales. Los emojis son cómodo para
practicar, pero un sitio de verdad lleva fotos. Reemplaza dos de los <div class="col">
por estos (usamos fotos de ejemplo de internet; tú pon las tuyas):
<div class="col">
<div class="ratio ratio-1x1">
<img src="https://picsum.photos/seed/pixelcrew-torneo/400/400"
class="rounded-4 object-fit-cover w-100 h-100"
alt="Jugadores alrededor de una mesa con consolas en un torneo del club"
width="400" height="400" loading="lazy" decoding="async">
</div>
</div>
Qué aprendiste ahí, y son cosas que se usan todo el tiempo:
| Atributo o clase | Para qué |
|---|---|
alt |
Describe la imagen para quien no la ve. Si es solo decorativa, se deja vacío: alt="" |
width y height |
El navegador reserva el espacio antes de descargar la foto: la página no "salta" al cargar |
loading="lazy" |
No descarga la foto hasta que está cerca de la pantalla. Acelera páginas largas |
object-fit-cover |
Rellena el cuadrado sin deformar la foto |
decoding="async" |
El navegador la decodifica sin bloquear el dibujado |
⚠️ alt no es un adorno. Escribir alt="foto 1" no sirve de nada: quien usa un
lector de pantalla solo oirá ese texto inútil. Describe lo que la imagen muestra.
#Resultado esperado
Una franja con 4 cifras y una galería de 6 cuadrados de colores. Al achicar la ventana, la franja pasa de 4 columnas a 2 + 2, y la galería de 6 a 3 + 3 y luego a 2 + 2 + 2.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| Las columnas se pegan a los bordes de la pantalla | Falta el .container |
Envuelve la fila en un .container |
| Las columnas no se alinean bien | Pusiste .col sin .row |
Las columnas siempre van dentro de una .row |
| Una columna salta de línea sin razón | Las columnas suman más de 12 | Revisa la suma de los números col-* |
col-xs-6 o col-sm-offset-2 no funcionan |
Son de Bootstrap 3 | En Bootstrap 5 es col-6 y offset-sm-2 |
| No ves cambios al achicar la ventana | Te falta la etiqueta viewport o estás mirando otro breakpoint |
Usa el modo responsive de F12 y revisa los anchos de la tabla |
| La página se desplaza a los lados en celular | Usaste un g-5 (gutter grande) dentro de un .container |
Una fila con gutter se come la mitad del gutter en margen negativo a cada lado. Si eso supera el relleno del contenedor, la fila se sale de la pantalla. Solución: g-4 g-lg-5 |
#Mini-ejercicio
Modifica la galería para que en pantallas lg se vean 4 cuadrados por fila (en vez de 6) y agrega 2 cuadrados más (con otro emoji y otro color) para completar 2 filas parejas de 4.
💡 Pista
Solo necesitas cambiar un número en `row-cols-lg-6` y duplicar dos bloques `✅ Solución
Cambia `row-cols-lg-6` por `row-cols-lg-4` y agrega, por ejemplo:<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-dark d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de una estrella">⭐</div>
</div>
</div>
💡 Puedes dejar la galería como la dejaste: el resto del tutorial no depende de cuántos cuadrados tenga.
#✅ Punto de control
- [ ] Puedo explicar la jerarquía
container → row → col. - [ ] Entiendo que
col-12 col-md-6 col-lg-4significa "mobile-first". - [ ] Mi franja de estadísticas pasa de 4 a 2 columnas en celular.
- [ ] Mi galería cambia el número de cuadrados por fila según el ancho.
- [ ] Sé usar el modo responsive de las herramientas del navegador.
- [ ] Sé por qué un
g-5dentro de un.containerpuede hacer scroll horizontal, y sé usarg-4 g-lg-5.
#Módulo 3: Tipografía, color y utilidades
⏱ 20 minutos
🎯 Objetivo: dar estilo a tu contenido usando clases utilitarias de Bootstrap, y construir la portada (hero) del sitio.
#Concepto
Las utilidades son clases que hacen una sola cosa. Combinándolas puedes diseñar casi todo sin escribir CSS. Las más importantes:
| Familia | Ejemplos | Qué hacen |
|---|---|---|
| Tipografía | display-1…display-6, lead, fs-1…fs-6, fw-bold, fw-semibold, text-uppercase, small |
Tamaño, peso y estilo del texto |
| Alineación | text-center, text-start, text-end |
Alineación horizontal |
| Color de texto | text-primary, text-body-secondary, text-white |
Color del texto |
| Color de fondo | bg-primary, bg-success-subtle, bg-body-tertiary |
Color de fondo |
| Espaciado | m-3, p-4, mt-2, px-5, py-3, ms-auto, gap-3 |
Márgenes, relleno y separación |
| Display y flex | d-flex, d-none, d-lg-block, flex-column, justify-content-between, align-items-center |
Mostrar, ocultar y alinear |
| Bordes y sombras | border, rounded-4, rounded-pill, shadow, shadow-lg |
Aspecto de las cajas |
#La escala de espaciado
Los números del 0 al 5 siguen esta escala:
| Número | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| Tamaño | 0 | 0,25 rem | 0,5 rem | 1 rem | 1,5 rem | 3 rem |
Y las letras indican dónde aplica:
m= margen (afuera),p= padding (adentro).tarriba,babajo,sinicio (izquierda),efinal (derecha),xizquierda y derecha,yarriba y abajo.- Sin letra = los cuatro lados.
Así, mt-4 es "margen arriba tamaño 4" y px-5 es "relleno horizontal tamaño 5". Y todas aceptan breakpoints: py-3 py-lg-5.
#Colores adaptables
Fíjate en que usamos text-body-secondary y bg-body-tertiary en vez de colores fijos como text-muted o bg-light. Los primeros cambian solos entre modo claro y oscuro, y son los recomendados desde Bootstrap 5.3.
💡 Regla práctica: usa utilidades para el 90 % del estilo y escribe CSS propio solo para lo que Bootstrap no trae (como un degradado).
#Pasos
1. Agrega el HTML del hero. Pega justo debajo de <!-- HERO -->:
<section id="inicio" class="hero py-5">
<div class="container py-lg-5">
<div class="row align-items-center g-4 g-lg-5">
<div class="col-lg-7">
<p class="text-uppercase fw-semibold text-primary-emphasis small mb-2">Club de videojuegos del colegio</p>
<h1 class="display-3 fw-bold mb-3">Juega, crea y compite con <span class="text-gradient">PixelCrew</span></h1>
<p class="lead text-body-secondary mb-4">
Somos el club donde los videojuegos se juegan, se estudian y hasta se programan.
Si tienes entre 14 y 18 años, hay un lugar para ti en el equipo.
</p>
<div class="d-flex flex-column flex-sm-row gap-3">
<a href="#unete" class="btn btn-primary btn-lg px-4">Únete al club</a>
<a href="#juegos" class="btn btn-outline-secondary btn-lg px-4">Descubre qué hacemos</a>
</div>
</div>
<div class="col-lg-5 d-none d-lg-block">
<div class="hero-card rounded-4 shadow-lg p-5 text-center text-white" aria-hidden="true">
<div class="display-1">🎮</div>
<p class="fs-4 fw-semibold mb-0">Nivel 1: ¡empieza hoy!</p>
</div>
</div>
</div>
</div>
</section>
Detalles para fijarte:
py-5ypy-lg-5en el contenedor: relleno vertical, más grande en pantallas grandes.display-3 fw-bold: el título enorme y en negrita.leadhace un párrafo destacado.d-flex flex-column flex-sm-row gap-3: los botones se apilan en celular y se ponen uno al lado del otro desdesm.d-none d-lg-block: la tarjeta decorativa solo aparece desde pantallas grandes.aria-hidden="true": es decoración, así que los lectores de pantalla la saltan.<h1>: solo hay uno por página.g-4 g-lg-5en la fila: en celular usamos un gutter mediano y lo subimos en pantallas grandes. ¿Por qué? Porque una fila cong-5tiene un margen negativo de 24 px a cada lado, más ancho que el relleno del contenedor, y eso hace que la página se desplace horizontalmente en el celular. Es un error muy común; pruébalo: cambia ag-5a secas, achica la ventana y verás cómo aparece la barra de desplazamiento horizontal.
2. Agrega CSS propio. Bootstrap no trae degradados, así que esta es una buena excusa para escribir un poco de CSS. Pega esto en css/style.css:
/* Un texto muy largo (un correo, una URL) puede empujar la página y crear
scroll horizontal en celular. `overflow-wrap: anywhere` lo parte.
Ojo: `anywhere`, no `break-word`, porque es el único que además reduce el
ancho mínimo de las celdas de un grid. */
body {
overflow-wrap: anywhere;
}
/* ===== 3. Hero ===== */
.hero {
background:
radial-gradient(circle at 15% 20%, rgba(124, 77, 255, 0.25), transparent 45%),
radial-gradient(circle at 85% 80%, rgba(13, 139, 217, 0.18), transparent 45%);
}
.text-gradient {
background: linear-gradient(90deg, #7c4dff, #0d8bd9);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero-card {
background: linear-gradient(135deg, #7c4dff, #0d8bd9);
}
La clase text-gradient pinta el texto con un degradado recortando el fondo con la forma de las letras.
#Resultado esperado
Una portada con título gigante (la palabra "PixelCrew" con degradado de morado a azul), un texto descriptivo en gris, dos botones y, en pantallas anchas, una tarjeta decorativa a la derecha. En celular la tarjeta desaparece y los botones ocupan todo el ancho.
#Errores comunes
⚠️ Clases que ya no existen o están obsoletas (las verás en tutoriales viejos):
| Si ves… | Usa… |
|---|---|
ml-3, mr-3 |
ms-3, me-3 |
pl-2, pr-2 |
ps-2, pe-2 |
text-left, text-right |
text-start, text-end |
float-left, float-right |
float-start, float-end |
font-weight-bold |
fw-bold |
text-muted |
text-body-secondary |
badge-primary |
text-bg-primary |
data-toggle, data-target |
data-bs-toggle, data-bs-target |
Otros problemas:
- El título no tiene degradado: revisa que
style.cssesté enlazado y que escribiste-webkit-background-clipybackground-clip. - Tu CSS no pisa a Bootstrap: confirma que
style.cssse carga después del CSS de Bootstrap. - La página se desplaza en celular: casi siempre es un
g-5dentro de un.containero un texto largo sin partir. Usag-4 g-lg-5y agregaoverflow-wrap: anywherealbody.
#Mini-ejercicio
En celular, centra el título y el párrafo del hero. Desde pantallas lg, deben volver a alinearse a la izquierda.
💡 Pista
Es una clase de alineación con y sin prefijo de breakpoint. Va en la columna que contiene el texto (`col-lg-7`).✅ Solución
Agrega `text-center text-lg-start` a la columna: `#✅ Punto de control
- [ ] Mi hero muestra el título con degradado y dos botones.
- [ ] Entiendo qué significa
mt-4,px-5ypy-lg-5. - [ ] Sé usar
d-none d-lg-blockpara ocultar elementos según el tamaño de pantalla. - [ ] Sé cuáles clases de Bootstrap 4 ya no debo usar.
#Módulo 4: Navbar y navegación responsive
⏱ 25 minutos
🎯 Objetivo: crear una barra de navegación que se colapsa en un menú hamburguesa en pantallas pequeñas y que permanece fija arriba.
#Concepto
La navbar es un componente completo de Bootstrap. Se compone de:
.navbar: el contenedor principal..navbar-expand-lg: define desde qué breakpoint el menú se muestra expandido. Por debajo delg, se colapsa..navbar-brand: el logo o nombre del sitio..navbar-toggler: el botón hamburguesa..navbar-collapse: la parte que se oculta y se muestra..navbar-nav,.nav-item,.nav-link: la lista de enlaces.
El botón hamburguesa funciona sin que escribas JavaScript: Bootstrap lee atributos data-bs-* en el HTML. Aquí, data-bs-toggle="collapse" y data-bs-target="#menuPrincipal" significan "al hacer clic, muestra u oculta el elemento con id menuPrincipal".
#Accesibilidad en la navbar
aria-labelen el<nav>y en el botón dice a los lectores de pantalla qué son.aria-expandedindica si el menú está abierto (Bootstrap lo actualiza solo).- Un enlace "Saltar al contenido" permite a quien navega con teclado evitar repetir todo el menú.
#Pasos
1. Pega la navbar justo debajo de <!-- NAVBAR -->:
<a class="visually-hidden-focusable" href="#contenido">Saltar al contenido</a>
<header class="sticky-top">
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom" aria-label="Navegación principal">
<div class="container">
<a class="navbar-brand fw-bold" href="#inicio">🎮 PixelCrew</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Abrir o cerrar el menú">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menuPrincipal">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 align-items-lg-center">
<li class="nav-item"><a class="nav-link" href="#juegos">Qué hacemos</a></li>
<li class="nav-item"><a class="nav-link" href="#galeria">Galería</a></li>
<li class="nav-item"><a class="nav-link" href="#testimonios">Comunidad</a></li>
<li class="nav-item"><a class="nav-link" href="#faq">Preguntas</a></li>
<li class="nav-item ms-lg-3 my-2 my-lg-0"><a class="btn btn-primary btn-sm" href="#unete">Únete</a></li>
</ul>
</div>
</div>
</nav>
</header>
2. Revisa los detalles:
sticky-top: la barra se queda fija arriba al hacer scroll.bg-body-tertiary border-bottom: un fondo suave que se adapta al modo claro/oscuro.ms-auto: empuja los enlaces hacia la derecha (el margen izquierdo es "automático").align-items-lg-center: alinea verticalmente solo desdelg.visually-hidden-focusable: el enlace de saltar está oculto hasta que lo enfocas con la teclaTab.
3. Pruébalo:
- Estira y encoge la ventana: pasado
lg(992 px), el menú se oculta tras el botón hamburguesa. - Haz clic en la hamburguesa: el menú se despliega.
- Los enlaces del menú todavía no te llevan a ningún lado: las secciones a las que apuntan las vas a crear en los próximos módulos.
- Presiona
Tabapenas cargue la página: debe aparecer "Saltar al contenido".
#Resultado esperado
Una barra superior con el nombre del club a la izquierda y los enlaces a la derecha. En celular, solo se ve el nombre y el botón de tres rayitas.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El botón hamburguesa no hace nada | Falta el bundle JS, o el id no coincide con data-bs-target |
Revisa que data-bs-target="#menuPrincipal" y id="menuPrincipal" sean idénticos |
| Los enlaces quedan a la izquierda | Falta ms-auto |
Agrégalo al <ul class="navbar-nav"> |
| El menú nunca se colapsa | Falta navbar-expand-lg o collapse |
Revisa ambos nombres de clase |
| La navbar tapa el contenido al navegar a una sección | Es normal con sticky-top |
Lo corregiremos con una línea de CSS en el módulo 8 |
| El ícono hamburguesa no aparece | Falta <span class="navbar-toggler-icon"> |
Agrégalo dentro del botón |
#Mini-ejercicio
Haz que el menú se expanda desde pantallas medianas (md) en lugar de lg.
💡 Pista
Cambia el breakpoint en una clase de la navbar. Hay otras clases con `lg` dentro de la navbar que también deberías ajustar para que todo sea coherente.✅ Solución
Cambia `navbar-expand-lg` por `navbar-expand-md`, y en el `- ` usa `mb-md-0` y `align-items-md-center`. También ajusta `ms-lg-3` y `my-lg-0` del botón "Únete" a `ms-md-3` y `my-md-0`.
#✅ Punto de control
- [ ] La navbar se ve completa en pantalla ancha y se colapsa en pantalla angosta.
- [ ] El botón hamburguesa abre y cierra el menú.
- [ ] La barra se queda fija arriba al hacer scroll.
- [ ] Entiendo cómo
data-bs-toggleydata-bs-targetconectan un botón con un elemento. - [ ] Probé el enlace "Saltar al contenido" con la tecla
Tab.
#Módulo 5: Tarjetas, botones, badges y alertas
⏱ 25 minutos
🎯 Objetivo: usar los componentes más comunes de Bootstrap para presentar información de forma atractiva.
#Concepto
Un componente es un bloque de interfaz listo para usar. Se arma con una clase base y variantes:
- Botones: base
.btn+ variante de color (.btn-primary) o contorno (.btn-outline-primary) + tamaño (.btn-lg,.btn-sm). - Badges: etiquetas pequeñas, con
.badge text-bg-warning(el fondo y el texto con buen contraste ya vienen resueltos). - Alertas: mensajes contextuales:
.alert alert-success,alert-danger,alert-warning,alert-info. Pueden cerrarse con un botón. - Tarjetas:
.card>.card-body>.card-title,.card-text. También.card-headery.card-footer.
Los colores contextuales significan algo, y es bueno respetarlo:
| Color | Significado |
|---|---|
primary |
Acción principal |
secondary |
Acción secundaria |
success |
Todo salió bien |
danger |
Error o peligro |
warning |
Atención |
info |
Información neutral |
Una tarjeta que ocupa toda la fila no es útil por sí sola; lo común es ponerlas dentro de un grid, como aprendiste en el módulo 2: row row-cols-1 row-cols-md-3 g-4. Y para que todas midan lo mismo de alto, cada tarjeta lleva h-100.
#Pasos
1. Agrega la sección "Qué hacemos". Pega justo debajo de <!-- QUÉ HACEMOS -->:
<section id="juegos" class="py-5">
<div class="container">
<div class="alert alert-info alert-dismissible fade show" role="alert">
<strong>¡Inscripciones abiertas!</strong> Hay cupos disponibles hasta el 31 de octubre.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar aviso"></button>
</div>
<div class="row justify-content-center my-5">
<div class="col-lg-7 text-center">
<h2 class="mb-2">Más que jugar</h2>
<p class="text-body-secondary mb-0">Tres formas de formar parte del club. Elige la tuya (o prueba las tres).</p>
</div>
</div>
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
<div class="card h-100 card-hover shadow-sm">
<div class="card-body">
<span class="badge text-bg-warning mb-3">Cada viernes</span>
<h3 class="card-title h5">🏆 Torneos</h3>
<p class="card-text text-body-secondary">Compite en equipo o en solitario en juegos para todos los niveles. Hay premios, diplomas y mucha rivalidad sana.</p>
</div>
<div class="card-footer bg-transparent border-0 pb-3">
<a href="#unete" class="btn btn-outline-primary w-100">Quiero competir</a>
</div>
</div>
</div>
<div class="col">
<div class="card h-100 card-hover shadow-sm">
<div class="card-body">
<span class="badge text-bg-success mb-3">Cada miércoles</span>
<h3 class="card-title h5">🕹️ Noches de juego</h3>
<p class="card-text text-body-secondary">Juegos cooperativos y de mesa, música y snacks. Ideal para conocer gente nueva sin presión.</p>
</div>
<div class="card-footer bg-transparent border-0 pb-3">
<a href="#unete" class="btn btn-outline-primary w-100">Me apunto</a>
</div>
</div>
</div>
<div class="col">
<div class="card h-100 card-hover shadow-sm">
<div class="card-body">
<span class="badge text-bg-info mb-3">Cada martes</span>
<h3 class="card-title h5">💻 Taller de creación</h3>
<p class="card-text text-body-secondary">Aprende a diseñar niveles y personajes, y a programar tu primer videojuego desde cero.</p>
</div>
<div class="card-footer bg-transparent border-0 pb-3">
<a href="#unete" class="btn btn-outline-primary w-100">Quiero crear</a>
</div>
</div>
</div>
</div>
</div>
</section>
Qué mirar:
- La alerta al inicio tiene
alert-dismissible fade showy un botónbtn-closecondata-bs-dismiss="alert": se cierra con un clic, sin que escribas JS. - Las tres tarjetas usan
h-100para igualar alturas ycard-footer bg-transparent border-0para un pie limpio. - Cada badge usa un color distinto según el día.
- Los títulos son
<h3 class="h5">: semánticamente son unh3(porque están debajo de unh2), pero se ven con tamañoh5. Es un truco muy útil. - Los botones
w-100ocupan todo el ancho de su tarjeta.
2. Agrega el efecto "levitar" al pasar el mouse. Pega al final de css/style.css:
/* ===== 4. Tarjetas ===== */
.card-hover {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card-hover:hover {
transform: translateY(-6px);
box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.25) !important;
}
@media (prefers-reduced-motion: reduce) {
.card-hover { transition: none; }
.card-hover:hover { transform: none; }
}
El bloque prefers-reduced-motion desactiva la animación para quienes tienen activada la opción de "reducir movimiento" en su dispositivo. Es una buena práctica de accesibilidad.
#Resultado esperado
Una alerta azul cerrable, un título centrado y tres tarjetas del mismo alto, con una etiqueta de color, un título con emoji, una descripción y un botón. Al pasar el mouse, cada tarjeta sube un poco. En celular, las tarjetas se apilan.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| Las tarjetas tienen alturas distintas | Falta h-100 en .card |
Agrégalo a cada .card |
| La alerta no se puede cerrar | Falta el bundle JS o data-bs-dismiss="alert" |
Revisa ambos |
| El texto del badge no se lee | Usaste bg-warning sin color de texto |
Usa text-bg-warning que ya incluye ambos |
| Las tarjetas ocupan todo el ancho | Olvidaste el row row-cols-* |
Envuélvelas en un grid |
btn btn-default no se ve |
No existe en Bootstrap 5 | Usa btn-secondary o btn-outline-secondary |
#Mini-ejercicio
Agrega una cuarta tarjeta llamada "📚 Zona de estudio" con un badge text-bg-danger que diga "Antes de exámenes". Haz que en pantallas lg se vean las 4 en una fila, y en md, 2 por fila.
💡 Pista
Duplica una `✅ Solución
Cambia la fila a `row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4` y agrega la cuarta tarjeta copiando la estructura de una existente (badge `text-bg-danger`, título "📚 Zona de estudio" y su texto).#✅ Punto de control
- [ ] Veo tres tarjetas del mismo alto dentro de un grid.
- [ ] La alerta se cierra con su botón.
- [ ] Entiendo para qué sirven
h-100,text-bg-*ybtn-close. - [ ] Sé elegir el color contextual correcto según el mensaje.
- [ ] Las tarjetas se apilan en celular.
#Módulo 6: Formularios y validación
⏱ 25 minutos
🎯 Objetivo: construir un formulario de inscripción accesible y validarlo con las herramientas del navegador y Bootstrap.
#Concepto
Bootstrap da estilo a los formularios con pocas clases:
| Clase | Para qué sirve |
|---|---|
.form-control |
Campos de texto, correo, número, textarea |
.form-select |
Listas desplegables |
.form-check + .form-check-input + .form-check-label |
Casillas y botones de opción |
.form-label |
Etiqueta sobre un campo |
.form-floating |
Etiqueta que "flota" dentro del campo |
.input-group + .input-group-text |
Un campo con un texto o ícono pegado (como el @) |
#Cómo funciona la validación
La validación tiene tres piezas:
- Atributos HTML5 que definen las reglas:
required,minlength,min,max,type="email",pattern. El navegador ya sabe evaluarlos. - JavaScript que pregunta al navegador si el formulario es válido con
formulario.checkValidity(). - La clase
.was-validateden el<form>. Al agregarla, Bootstrap pinta de verde los campos válidos y de rojo los inválidos, y muestra los mensajes.invalid-feedback.
También agregamos novalidate al <form> para apagar los globitos feos del navegador y mostrar los mensajes de Bootstrap en su lugar.
#Accesibilidad en formularios
Todo campo necesita una <label> conectada con el id del campo (for="nombre" ↔ id="nombre"). Sin eso, los lectores de pantalla no saben qué se pide, y hacer clic en la etiqueta no enfoca el campo. En el campo @ que no tiene etiqueta visible usamos aria-label.
#Pasos
1. Agrega el formulario. Pega justo debajo de <!-- ÚNETE -->:
<section id="unete" class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-7">
<h2 class="text-center mb-2">Únete a PixelCrew</h2>
<p class="text-center text-body-secondary mb-4">Completa el formulario y te contamos cuándo es la primera reunión.</p>
<div id="exito" class="alert alert-success d-none" role="status" tabindex="-1">
🎉 ¡Listo! Recibimos tu inscripción. Nos vemos en el siguiente nivel.
</div>
<form id="formUnete" class="row g-3" novalidate>
<div class="col-md-6">
<div class="form-floating">
<input type="text" class="form-control" id="nombre" placeholder="Tu nombre" required minlength="2" autocomplete="given-name">
<label for="nombre">Nombre</label>
<div class="invalid-feedback">Escribe tu nombre (mínimo 2 letras).</div>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<input type="email" class="form-control" id="correo" placeholder="tucorreo@ejemplo.com" required autocomplete="email">
<label for="correo">Correo electrónico</label>
<div class="invalid-feedback">Escribe un correo válido, por ejemplo nombre@correo.com.</div>
</div>
</div>
<div class="col-md-4">
<div class="form-floating">
<input type="number" class="form-control" id="edad" placeholder="16" required min="13" max="18">
<label for="edad">Edad</label>
<div class="invalid-feedback">El club es para edades entre 13 y 18 años.</div>
</div>
</div>
<div class="col-md-8">
<div class="form-floating">
<select class="form-select" id="interes" required>
<option value="" selected disabled>Elige una opción…</option>
<option>Torneos</option>
<option>Noches de juego</option>
<option>Taller de creación</option>
<option>Todo lo anterior</option>
</select>
<label for="interes">¿Qué te interesa más?</label>
<div class="invalid-feedback">Elige una opción de la lista.</div>
</div>
</div>
<div class="col-12">
<div class="input-group">
<span class="input-group-text" id="arroba">@</span>
<input type="text" class="form-control" id="usuario" placeholder="Tu usuario gamer (opcional)" aria-label="Tu usuario gamer (opcional)" aria-describedby="arroba">
</div>
</div>
<div class="col-12">
<label for="mensaje" class="form-label">Cuéntanos tu juego favorito (opcional)</label>
<textarea class="form-control" id="mensaje" rows="3"></textarea>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="reglas" required>
<label class="form-check-label" for="reglas">Acepto las reglas del club</label>
<div class="invalid-feedback">Debes aceptar las reglas para inscribirte.</div>
</div>
</div>
<div class="col-12 d-grid">
<button class="btn btn-primary btn-lg" type="submit">Enviar inscripción</button>
</div>
</form>
</div>
</div>
</div>
</section>
Qué observar:
row g-3+col-md-6: ¡el grid también sirve para formularios! Dos campos por fila en pantallas medianas, uno por fila en celular.- El
<select>tiene una primera opción convalue=""ydisabled selected: es el "texto guía". Como está vacía yrequired, obliga a elegir una opción real. edadusamin="13" max="18": el navegador rechaza valores fuera de rango.d-noneen la alerta de éxito: está oculta hasta que se necesite.d-griden el botón: lo estira a todo el ancho.
2. Agrega el JavaScript. Pega en js/main.js:
// ===== Validación del formulario de inscripción =====
const formulario = document.getElementById('formUnete');
const mensajeExito = document.getElementById('exito');
formulario.addEventListener('submit', (evento) => {
evento.preventDefault(); // este sitio de práctica no envía datos a ningún servidor
if (formulario.checkValidity()) {
formulario.reset();
formulario.classList.remove('was-validated');
mensajeExito.classList.remove('d-none');
mensajeExito.focus(); // el foco va al mensaje: quien navega con
// teclado o lector de pantalla se entera
} else {
mensajeExito.classList.add('d-none');
formulario.classList.add('was-validated');
// Nadie se da cuenta de un error si no lo ve. Movemos el foco al primer
// campo inválido: el navegador lo resalta y además lo deja preparado
// para escribir de nuevo.
const primerError = formulario.querySelector(':invalid');
if (primerError) primerError.focus();
}
});
Léelo con calma. No es mucho código:
getElementByIdbusca los elementos por suid.addEventListener('submit', ...)ejecuta una función cuando se intenta enviar el formulario.evento.preventDefault()evita que la página se recargue.formulario.checkValidity()le pregunta al navegador si todos los campos cumplen las reglas.formulario.querySelector(':invalid')devuelve el primer campo que no las cumple.- Si es válido: limpia el formulario, muestra el mensaje de éxito y le da el foco.
- Si no: activa
was-validatedy lleva el foco al primer campo con error.
#Por qué importa lo del foco
Marcar los campos en rojo no sirve de nada si la persona sigue mirando el botón. Al mandar el foco al primer error, quien usa teclado o lector de pantalla escucha "Correo electrónico, inválido" y puede corregirlo sin buscarlo con el mouse.
Para que mensajeExito.focus() funcione, el mensaje necesita poder recibir foco, y eso
se logra con tabindex="-1". Es el valor especial que significa: "puede recibir el foco
por código, pero no aparece en la secuencia del Tab".
3. Pruébalo así:
- Presiona Enviar inscripción con todo vacío → campos en rojo con sus mensajes.
- Escribe un correo mal escrito (
ana@) → sigue en rojo. - Escribe edad
25→ rojo; con16→ verde. - Completa todo correctamente y marca las reglas → aparece el mensaje de éxito.
⚠️ Sobre datos personales: este formulario no guarda ni envía nada. En un sitio real que recoja datos de menores de edad se necesita permiso de madres, padres o tutores y un manejo cuidadoso de la información. Nunca publiques datos personales de otras personas.
#Resultado esperado
Un formulario centrado con campos de etiqueta flotante. Al enviarlo vacío, todo lo obligatorio se marca en rojo con su mensaje explicativo. Al completarlo bien, el formulario se limpia y aparece una alerta verde.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| Aparecen globitos del navegador en vez de los mensajes de Bootstrap | Falta novalidate en el <form> |
Agrégalo |
| Nada se pone rojo ni verde | No se agregó was-validated o main.js no está enlazado |
Revisa la consola (F12) por errores y confirma que el script esté al final del <body> |
| El mensaje de error nunca aparece | El .invalid-feedback no está después del campo, como hermano |
Colócalo justo después del <input> o del <label> |
| Los campos flotantes se ven mal | Falta placeholder en el input |
Los form-floating requieren un placeholder (aunque sea con una palabra) |
Cannot read properties of null en la consola |
El id en JS no coincide con el HTML |
Compara formUnete y exito en ambos |
Usas form-group o form-row |
Son de Bootstrap 4 | En Bootstrap 5: mb-3 o row g-3 |
| Los campos se ponen rojos pero el foco sigue en el botón | Falta la línea que mueve el foco al primer :invalid |
Agrega el querySelector(':invalid') con su focus() |
mensajeExito.focus() no hace nada |
El mensaje no puede recibir foco | Agrega tabindex="-1" al div del mensaje |
querySelector(':invalid') devuelve null |
El formulario es válido | Solo llámalo dentro del else |
#Mini-ejercicio
Haz que el campo usuario gamer (opcional) solo acepte letras, números y guion bajo, de 3 a 16 caracteres. Si escriben algo distinto, debe mostrar un mensaje de error. Si lo dejan vacío, debe seguir siendo válido.
💡 Pista
Existe un atributo HTML5 llamado `pattern` que recibe una expresión regular. Para que el mensaje de error se vea bien dentro de un `.input-group`, agrega la clase `has-validation` al grupo y pon el `.invalid-feedback` **dentro** del grupo.✅ Solución
<div class="input-group has-validation">
<span class="input-group-text" id="arroba">@</span>
<input type="text" class="form-control" id="usuario"
placeholder="Tu usuario gamer (opcional)"
aria-label="Tu usuario gamer (opcional)" aria-describedby="arroba"
pattern="[A-Za-z0-9_]{3,16}">
<div class="invalid-feedback">Usa de 3 a 16 letras, números o guion bajo.</div>
</div>
#✅ Punto de control
- [ ] Mi formulario tiene todos sus campos con etiqueta asociada.
- [ ] Al enviarlo vacío se ven los errores en rojo con mensajes.
- [ ] Al llenarlo bien aparece el mensaje de éxito y se limpia.
- [ ] Entiendo la función de
required,novalidate,checkValidity()ywas-validated. - [ ] Sé por qué hay que mover el foco al primer error y al mensaje de éxito.
- [ ] Sé que no debo pedir ni publicar datos personales sin cuidado.
#Módulo 7: Componentes interactivos con JavaScript
⏱ 25 minutos
🎯 Objetivo: usar el JavaScript de Bootstrap para crear un acordeón, un carrusel y una ventana modal.
#Concepto
Bootstrap trae su propio JavaScript para los componentes interactivos. Puedes usarlo de dos maneras:
| Forma | Cómo | Cuándo |
|---|---|---|
Atributos data-bs-* |
Solo HTML, sin escribir JS | La mayoría de los casos (lo que usaremos) |
| API de JavaScript | bootstrap.Modal.getOrCreateInstance(...) |
Cuando necesitas abrir o cerrar con tu propia lógica |
Los atributos más importantes:
| Atributo | Significado |
|---|---|
data-bs-toggle="modal" / "collapse" |
Qué tipo de componente activa este botón |
data-bs-target="#id" |
A qué elemento afecta |
data-bs-dismiss="modal" / "alert" |
Cierra el componente |
data-bs-slide="prev" / "next" |
Mueve el carrusel |
data-bs-parent="#id" |
En un acordeón, hace que solo se abra uno a la vez |
La regla de oro: los id deben coincidir exactamente entre el botón y el elemento al que apunta, y no pueden repetirse en la página.
#Pasos
1. Acordeón de preguntas frecuentes. Pega debajo de <!-- PREGUNTAS -->:
<section id="faq" class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<h2 class="text-center mb-4">Preguntas frecuentes</h2>
<div class="accordion" id="acordeonFaq">
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1" aria-expanded="true" aria-controls="faq1">
¿Necesito experiencia para entrar?
</button>
</h3>
<div id="faq1" class="accordion-collapse collapse show" data-bs-parent="#acordeonFaq">
<div class="accordion-body">Para nada. Hay miembros que juegan desde hace años y otros que nunca han tenido una consola. Te ayudamos a empezar.</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2" aria-expanded="false" aria-controls="faq2">
¿Cuánto cuesta?
</button>
</h3>
<div id="faq2" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
<div class="accordion-body">Unirte es gratis. Solo pedimos que cuides el material y participes en las actividades.</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3" aria-expanded="false" aria-controls="faq3">
¿Cuándo nos reunimos?
</button>
</h3>
<div id="faq3" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
<div class="accordion-body">Los martes, miércoles y viernes después de clases. Te enviamos el calendario completo cuando te inscribas.</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4" aria-expanded="false" aria-controls="faq4">
¿Necesito mi propio equipo?
</button>
</h3>
<div id="faq4" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
<div class="accordion-body">No. El club tiene consolas, computadores y mandos para todos. Si quieres traer el tuyo, ¡bienvenido!</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
Fíjate en el patrón: cada botón apunta (data-bs-target="#faq1") a su panel (id="faq1"). El primer ítem tiene show (abierto) y los demás collapsed en el botón.
2. Carrusel de testimonios. Pega debajo de <!-- COMUNIDAD -->:
<section id="testimonios" class="py-5 bg-body-tertiary">
<div class="container">
<h2 class="text-center mb-4">Lo que dice la comunidad</h2>
<div class="row justify-content-center">
<div class="col-lg-8">
<div id="carruselComunidad" class="carousel slide" aria-label="Testimonios de miembros del club">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Testimonio 1"></button>
<button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="1" aria-label="Testimonio 2"></button>
<button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="2" aria-label="Testimonio 3"></button>
</div>
<div class="carousel-inner rounded-4 shadow">
<div class="carousel-item active">
<figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
<blockquote class="fs-4 mb-3">“Entré por los torneos y salí sabiendo hacer mi primer videojuego.”</blockquote>
<figcaption class="fw-semibold">Valentina, 16 años</figcaption>
</figure>
</div>
<div class="carousel-item">
<figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
<blockquote class="fs-4 mb-3">“Aquí conocí a mis mejores amigos. Jugar en equipo es otro nivel.”</blockquote>
<figcaption class="fw-semibold">Santiago, 17 años</figcaption>
</figure>
</div>
<div class="carousel-item">
<figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
<blockquote class="fs-4 mb-3">“Pensé que era solo jugar, pero también aprendí a organizar eventos.”</blockquote>
<figcaption class="fw-semibold">Camila, 15 años</figcaption>
</figure>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carruselComunidad" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Anterior</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carruselComunidad" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Siguiente</span>
</button>
</div>
<p class="text-center text-body-secondary small mt-3 mb-0">Los nombres de este ejemplo son ficticios.</p>
</div>
</div>
</div>
</section>
El carrusel tiene tres partes: los indicadores (puntitos de abajo), las diapositivas (carousel-item; una debe llevar active) y los controles (flechas). Usamos botones con texto oculto (visually-hidden) para que las flechas sean comprensibles con lectores de pantalla. No lo configuramos con avance automático a propósito: el movimiento solo se activa por decisión de quien usa la página.
Agrega estilo a las diapositivas. Pega al final de css/style.css:
/* ===== 5. Carrusel de la comunidad ===== */
.testimonio {
min-height: 280px;
background: linear-gradient(135deg, #4b2bb5, #0b6fa8);
}
/* En modo oscuro Bootstrap pone controles negros en los carruseles.
Como nuestras diapositivas son oscuras en ambos modos, los forzamos a blanco. */
.carousel {
--bs-carousel-indicator-active-bg: #fff;
--bs-carousel-caption-color: #fff;
--bs-carousel-control-icon-filter: none;
}
3. Modal de reglas. Primero pega el modal justo debajo de <!-- MODAL -->:
<div class="modal fade" id="modalReglas" tabindex="-1" aria-labelledby="tituloReglas" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="tituloReglas">📜 Reglas de PixelCrew</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<ol class="mb-0">
<li class="mb-2"><strong>Respeto ante todo.</strong> Nada de insultos ni burlas, ni dentro ni fuera del juego.</li>
<li class="mb-2"><strong>Juego limpio.</strong> Sin trampas, hacks ni cuentas prestadas en los torneos.</li>
<li class="mb-2"><strong>Equilibrio.</strong> Las tareas del colegio van primero; el club puede esperar.</li>
<li class="mb-2"><strong>Cuidamos el material.</strong> Consolas, mandos y equipos son de todos.</li>
<li class="mb-2"><strong>Contenido apto.</strong> Los juegos deben ser adecuados para la edad de todos.</li>
<li><strong>Si algo te incomoda</strong>, avísale a una persona responsable del club. Siempre te vamos a escuchar.</li>
</ol>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Entendido</button>
</div>
</div>
</div>
</div>
Luego conecta un botón con él. En el formulario del módulo anterior, reemplaza esta línea:
<label class="form-check-label" for="reglas">Acepto las reglas del club</label>
por esta, que convierte "reglas del club" en un botón que abre el modal:
<label class="form-check-label" for="reglas">
Acepto las
<button type="button" class="btn btn-link p-0 align-baseline" data-bs-toggle="modal" data-bs-target="#modalReglas">reglas del club</button>
</label>
Puntos importantes del modal:
fadeda una animación suave al aparecer.modal-dialog-centeredlo centra verticalmente ymodal-dialog-scrollablepermite desplazar el contenido si es largo.aria-labelledby="tituloReglas"conecta el modal con su título para lectores de pantalla.- Bootstrap se encarga solo de atrapar el foco dentro del modal, de cerrarlo con la tecla
Escy de devolver el foco al botón que lo abrió cuando se cierra. tabindex="-1"en el modal no es un detalle: le permite a Bootstrap poner el foco ahí al abrirlo. Si lo quitas, el foco se pierde y la persona que navega con teclado se queda sin saber dónde está.- El modal se coloca al final del
<body>, fuera de cualquier sección, para evitar problemas de posición.
4. Prueba la API de JavaScript. Abre la consola (F12) y escribe:
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modalReglas'));
modal.show();
Se abre el modal desde código. Ciérralo con modal.hide(). Cuando una acción (por ejemplo, un temporizador) debe abrir un componente, usarás esta forma.
#Resultado esperado
- Acordeón: al abrir una pregunta, la anterior se cierra.
- Carrusel: puedes pasar de testimonio con las flechas o los puntitos.
- Modal: al hacer clic en "reglas del club" aparece una ventana centrada con el fondo oscurecido; se cierra con ✕, con "Entendido", con
Esco haciendo clic afuera.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El carrusel no muestra nada | Ninguna diapositiva tiene active |
Pon active en la primera carousel-item |
| Los puntitos no cambian de diapositiva | data-bs-slide-to no coincide con el número de diapositivas |
Cuéntalas: empiezan en 0 |
| El modal no abre | El data-bs-target no coincide con el id |
Compara #modalReglas con id="modalReglas" |
| El fondo del modal queda gris y bloqueado | Un error de JS dejó el modal a medias | Recarga y revisa la consola |
| Se abren varias preguntas a la vez | Falta data-bs-parent |
Agrégalo a cada accordion-collapse |
| Dos acordeones se afectan entre sí | Repetiste id |
Cada id debe ser único |
#Mini-ejercicio
- Haz que el acordeón permita tener varias preguntas abiertas a la vez.
- Haz que el carrusel cambie de diapositiva con un efecto de desvanecimiento en vez de deslizamiento.
💡 Pista
Para el 1, piensa qué atributo es el que "cierra a las demás". Para el 2, busca en la documentación oficial de Carousel el modificador `carousel-fade`.✅ Solución
1. Quita el atributo `data-bs-parent="#acordeonFaq"` de cada `accordion-collapse`. 2. Cambia `class="carousel slide"` por `class="carousel slide carousel-fade"`.#✅ Punto de control
- [ ] El acordeón abre una pregunta a la vez.
- [ ] El carrusel cambia con las flechas y con los puntitos.
- [ ] El modal abre desde el enlace de las reglas y se cierra de varias maneras.
- [ ] Entiendo qué relación hay entre
data-bs-targeteid. - [ ] Abrí el modal desde la consola con
bootstrap.Modal.
#Módulo 8: Personalización, modo oscuro y Sass
⏱ 20 minutos
🎯 Objetivo: cambiar la apariencia de Bootstrap con tus colores, activar un botón de modo claro/oscuro y entender cuándo conviene usar Sass.
#Concepto
Hasta ahora tu sitio se ve "como Bootstrap". Para que se vea tuyo hay tres niveles de personalización, de menor a mayor esfuerzo:
| Nivel | Cómo | Cuándo |
|---|---|---|
| 1. Utilidades | Clases como bg-primary, rounded-4 |
Siempre |
| 2. Variables CSS | Sobrescribes --bs-* en tu style.css |
Cambios de color, bordes y fuentes |
| 3. Sass | Compilas Bootstrap con tus valores | Proyectos grandes con tema propio |
#Variables CSS
Desde la versión 5.3, Bootstrap guarda sus colores, bordes y fuentes en variables CSS (--bs-primary, --bs-body-bg, --bs-border-radius...). Al cambiar una variable, todo lo que la usa cambia. Puedes verlas en las herramientas del navegador: selecciona la etiqueta <html> en la pestaña Elements y mira el panel Styles.
Hay dos tipos:
- Globales (en
:root): afectan a todo el sitio. Ejemplo:--bs-border-radius. - Locales de un componente: viven dentro de cada componente. Ejemplo: un botón usa
--bs-btn-bg. Para cambiar solo los botones primarios, sobrescribes esa variable en.btn-primary.
#Modo claro y oscuro
Bootstrap 5.3 trae dos temas integrados. Se elige con el atributo data-bs-theme (light o dark) en <html> o en cualquier elemento: el tema se aplica a ese elemento y a todo lo que contiene. Por eso para alternar el modo solo hay que cambiar un atributo con JavaScript, y recordarlo con localStorage.
Respetar el tema del sistema
Hay una cosa que un sitio bien hecho sí o sí hace: adoptar el tema que ya eligió la
persona en su dispositivo. Se lee con matchMedia:
window.matchMedia('(prefers-color-scheme: dark)').matches // true si el sistema está en oscuro
Ese dark es el tema del sistema. También existe (prefers-color-scheme: light),
(prefers-reduced-motion: reduce) (que ya usaste en el módulo 5) y otras como
(prefers-contrast: more). Todas son la manera correcta de "preguntarle" al dispositivo.
La regla de un sitio bien construido es esta:
Primero respetas el tema del sistema. Después, si la persona elige otro con tu botón, su elección manda (y se recuerda).
El parpadeo: por qué el script va en el <head>
Un problema típico: si el tema lo aplica JavaScript al final de la página, el navegador primero pinta el sitio en claro y un instante después lo cambia a oscuro. Se ve un parpadeo Diez unpleasantísimo.
La solución es la que pusimos en el módulo 1: un <script> diminuto en el <head>, antes
del CSS, que aplica el tema antes de que se pinte el primer píxel. Ese script debe ser
cortísimo y no depender de nada: por eso va dentro de una función immediately ejecutada
((function () { ... })()) y protegido con try/catch, para que un navegador en modo
privado (donde localStorage puede estar bloqueado) no rompa la página.
#Pasos
1. Agrega el botón de modo. En la navbar, pega después del <li> del botón "Únete" (dentro del mismo <ul>):
<li class="nav-item ms-lg-2 mb-2 mb-lg-0">
<button id="themeToggle" class="btn btn-outline-secondary btn-sm" type="button" aria-label="Cambiar entre modo claro y modo oscuro">
<span id="themeIcon" aria-hidden="true">☀️</span>
</button>
</li>
2. Agrega el JavaScript al final de js/main.js:
// ===== Modo claro / oscuro =====
const raiz = document.documentElement;
const botonTema = document.getElementById('themeToggle');
const iconoTema = document.getElementById('themeIcon');
const CLAVE = 'pc-tema'; // el nombre con el que guardamos la preferencia
// El tema que ya tiene el dispositivo. Se consulta, no se supone.
const consultaSistema = window.matchMedia('(prefers-color-scheme: dark)');
// Leer y guardar puede fallar en modo privado: por eso van protegidos.
function leerTemaGuardado() {
try {
return localStorage.getItem(CLAVE);
} catch (error) {
return null;
}
}
function aplicarTema(tema, recordar) {
raiz.setAttribute('data-bs-theme', tema);
iconoTema.textContent = tema === 'dark' ? '☀️' : '🌙'; // el ícono muestra a qué modo cambiarás
// Solo guardamos cuando la ELECCIÓN es de la persona. Si guardáramos el
// tema del sistema, después no sabríamos si ella eligió o si fue el default.
if (recordar) {
try {
localStorage.setItem(CLAVE, tema);
} catch (error) {
/* en modo privado no se guarda, pero el sitio funciona igual */
}
}
}
// Al cargar: primero la elección de la persona; si no hay, la del sistema.
aplicarTema(leerTemaGuardado() || (consultaSistema.matches ? 'dark' : 'light'), false);
botonTema.addEventListener('click', () => {
const actual = raiz.getAttribute('data-bs-theme');
aplicarTema(actual === 'dark' ? 'light' : 'dark', true);
});
// Si la persona NO ha elegido nada y cambia el tema del sistema, la seguimos.
consultaSistema.addEventListener('change', (evento) => {
if (!leerTemaGuardado()) {
aplicarTema(evento.matches ? 'dark' : 'light', false);
}
});
Qué hace, en orden:
- Consulta el tema del dispositivo con
matchMedia('(prefers-color-scheme: dark)'). - Aplica el guardado; si no hay, el del sistema.
- Cambia el emoji del botón.
- Guarda la preferencia (solo si la elección fue suya), protegida con
try/catch. - Escucha los cambios del tema del sistema, pero solo si la persona no ha elegido nada.
⚠️ El detalle que hace que la última parte funcione es el parámetro recordar. Si al
cargar guardáramos el tema del sistema, leerTemaGuardado() devolvería siempre un valor y
el sitio dejaría de seguir al sistema después del primer clic. Por eso el valor por
defecto se aplica con recordar = false.
💡 El try/catch parece exagerado, y sin embargo es lo correcto: en modo incógnito,
localStorage puede lanzar un error. Sin esa protección, una sola línea detiene todo el
archivo de JavaScript, y con ella se rompen también el formulario y el menú.
3. Personaliza con variables CSS. Pega al inicio de css/style.css (antes de lo que ya escribiste):
/* ===== 1. Variables: personalizamos Bootstrap sin tocar sus archivos ===== */
:root {
--pc-accent: #7c4dff;
/* Variables de Bootstrap que sobrescribimos */
--bs-primary: #7c4dff;
--bs-primary-rgb: 124, 77, 255;
--bs-primary-text-emphasis: #4a22b8;
--bs-primary-bg-subtle: #e9e0ff;
--bs-primary-border-subtle: #cbb8ff;
--bs-border-radius: 0.75rem;
}
/* Este bloque va DESPUÉS de :root para ganar en modo oscuro */
[data-bs-theme="dark"] {
--bs-body-bg: #0e0f1f;
--bs-body-bg-rgb: 14, 15, 31;
--bs-tertiary-bg: #161833;
--bs-tertiary-bg-rgb: 22, 24, 51;
--bs-primary-text-emphasis: #b8a0ff;
--bs-primary-bg-subtle: #241a4d;
--bs-primary-border-subtle: #4a2fa8;
}
/* Los componentes de Bootstrap guardan sus colores en variables LOCALES.
Las cambiamos para que los botones también usen nuestro morado. */
.btn-primary {
--bs-btn-bg: var(--pc-accent);
--bs-btn-border-color: var(--pc-accent);
--bs-btn-hover-bg: #6a3de8;
--bs-btn-hover-border-color: #6a3de8;
--bs-btn-active-bg: #5a30d0;
--bs-btn-active-border-color: #5a30d0;
--bs-btn-disabled-bg: var(--pc-accent);
--bs-btn-disabled-border-color: var(--pc-accent);
--bs-btn-focus-shadow-rgb: 124, 77, 255;
}
.btn-outline-primary {
--bs-btn-color: var(--bs-primary-text-emphasis);
--bs-btn-border-color: var(--pc-accent);
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: var(--pc-accent);
--bs-btn-hover-border-color: var(--pc-accent);
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #5a30d0;
--bs-btn-active-border-color: #5a30d0;
--bs-btn-focus-shadow-rgb: 124, 77, 255;
}
.card {
--bs-card-bg: var(--bs-tertiary-bg);
}
/* ===== 2. Ajustes generales ===== */
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
/* Evita que la navbar fija tape los títulos al navegar por el menú */
section[id] { scroll-margin-top: 4.5rem; }
Observa los dos patrones:
- Variables globales (
--bs-primary,--bs-body-bg...) que definen la paleta, una para el modo claro (:root) y otra para el oscuro ([data-bs-theme="dark"]). - Variables locales de componentes (
.btn-primary,.btn-outline-primary) para que los botones también adopten el morado.
⚠️ Cambiar --bs-primary no cambia el color de .btn-primary, porque los botones tienen su propia copia del color. Por eso fue necesario sobrescribir también --bs-btn-bg y sus variantes. Si tuvieras que hacer esto con muchos componentes, ahí es donde conviene Sass.
También verás que section[id] { scroll-margin-top } resuelve el problema del módulo 4: la navbar fija ya no tapa los títulos al navegar.
4. Introducción a Sass (lectura, opcional para practicar).
Sass es un lenguaje que se compila a CSS. Con Bootstrap te permite cambiar sus variables internas antes de generar el CSS, y entonces toda la paleta (botones, alertas, fondos suaves, bordes...) se recalcula automáticamente.
El flujo es:
# 1. Crear un proyecto e instalar Bootstrap y Sass (necesitas Node.js)
npm init -y
npm install bootstrap@5.3.8 sass
// scss/custom.scss
// 2. Tus variables ANTES de importar Bootstrap
$primary: #7c4dff;
$border-radius: 0.75rem;
// 3. Importa Bootstrap
@import "bootstrap/scss/bootstrap";
# 4. Compila a CSS
npx sass --load-path=node_modules scss/custom.scss css/custom.css
Luego enlazas css/custom.css en lugar del CSS del CDN.
⚠️ Esto es solo lectura, no lo hagas todavía. Si lo intentas hoy vas a ver una montaña
de avisos rojos: Bootstrap 5.3 todavía usa @import (la sintaxis vieja) y el compilador de
Sass moderno la marca como deprecada. Los avisos no rompen nada, pero desconciertan
mucho. Además compilar Sass necesita Node.js instalado, y en un portátil del colegio eso
puede ser un problema.
La ruta moderna es @use, pero Bootstrap todavía no la usa por dentro. Por ahora quédate
con las variables CSS del paso 3, que no necesitas compilar nada y hacen el 90 % de lo que
querrías hacer con Sass. Cuando tengas un proyecto propio y Node.js funcionando, vuelve
aquí.
💡 ¿Cuándo usar cada una? Variables CSS para ajustes rápidos y para cambiar de tema en tiempo real. Sass cuando quieres una identidad visual completa y estás en un proyecto con herramientas de desarrollo.
#Resultado esperado
El sitio tiene una paleta morada. En la barra hay un botón con ☀️ (en modo oscuro) o 🌙 (en modo claro) que cambia todo el sitio. Al recargar, se mantiene el modo elegido. Los botones principales son morados y las esquinas son un poco más redondeadas.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| Mis variables no se aplican | style.css se carga antes que Bootstrap |
Debe ir después en el <head> |
| El botón de modo no hace nada | main.js falló antes |
Revisa la consola: un error en el formulario detiene todo el archivo |
Cannot read properties of null (reading 'addEventListener') |
El botón no existe en el HTML | Confirma que pegaste el <li> con id="themeToggle" |
| El modo oscuro no recuerda mi elección | localStorage bloqueado por el navegador |
Prueba con otro navegador o con Live Server |
Cambié --bs-primary pero los botones siguen azules |
Los botones usan variables locales | Sobrescribe también --bs-btn-bg en .btn-primary |
| En modo oscuro algunos colores no cambian | Pusiste tu bloque oscuro antes que :root |
[data-bs-theme="dark"] debe ir después de :root |
| El sitio arranca siempre en oscuro, aunque tu sistema esté en claro | Usaste || 'dark' como valor por defecto |
Deja que el default sea el del sistema: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' |
| El sitio parpadea al cargar | El tema se aplica con JavaScript al final de la página | Pon el script del <head> (módulo 1), antes del CSS |
SecurityError o localStorage is not defined |
Modo privado con almacenamiento bloqueado | Ya está protegido con try/catch; si ves esto, revisa que lo pegaste bien |
| Cambias el tema del sistema y el sitio no se mueve | La persona ya había elegido con el botón | Es lo correcto: su elección manda. Borra pc-tema en la consola para volver al del sistema |
#Mini-ejercicio
- Cambia el radio de las esquinas de todo el sitio (botones, tarjetas, campos) con una sola línea.
- Cambia la fuente del sitio por una de sistema como
Georgia, serif.
💡 Pista
Mira la lista de variables `--bs-*` en el panel Styles. Hay una variable de radio de borde y otra de familia de fuente del cuerpo (`--bs-body-font-family`).✅ Solución
En `:root` de tu `style.css`::root {
--bs-border-radius: 1.25rem;
--bs-body-font-family: Georgia, serif;
}
#✅ Punto de control
- [ ] Mi sitio alterna entre modo claro y oscuro con el botón.
- [ ] El modo elegido se mantiene al recargar.
- [ ] Si borro la preferencia guardada, el sitio arranca con el tema de mi dispositivo.
- [ ] Sé por qué el script del tema va en el
<head>y no al final. - [ ] Los botones y los colores principales usan mi paleta.
- [ ] Entiendo la diferencia entre variables globales y variables locales de componente.
- [ ] Sé para qué sirve Sass y cuándo conviene usarlo.
#Módulo 9: Integración final y buenas prácticas
⏱ 15 minutos
🎯 Objetivo: terminar el sitio con el pie de página y revisarlo como lo haría un profesional: accesibilidad, orden del código y pruebas en distintos tamaños.
#Concepto
Un sitio "terminado" no es el que se ve bien en tu computador, sino el que funciona para todas las personas y en todos los dispositivos. Hay tres frentes de revisión: estructura y accesibilidad, responsive y calidad del código.
#Pasos
1. Agrega el pie de página. Pega debajo de <!-- FOOTER -->:
<footer class="border-top py-4 mt-5">
<div class="container d-flex flex-column flex-md-row justify-content-between align-items-center gap-3">
<p class="mb-0 text-body-secondary small">© 2026 PixelCrew · Hecho con Bootstrap 5.3.8</p>
<ul class="nav">
<li class="nav-item"><a class="nav-link px-2" href="#juegos">Qué hacemos</a></li>
<li class="nav-item"><a class="nav-link px-2" href="#faq">Preguntas</a></li>
<li class="nav-item"><a class="nav-link px-2" href="#unete">Únete</a></li>
</ul>
<a href="#inicio" class="btn btn-outline-secondary btn-sm">Volver arriba ↑</a>
</div>
</footer>
Detalles: flex-column flex-md-row apila los elementos en celular y los pone en línea desde tablet, y justify-content-between los reparte a los extremos. mt-5 separa el pie del contenido.
2. Haz la revisión de accesibilidad. Recorre esta lista con tu sitio abierto:
| Qué revisar | Cómo comprobarlo |
|---|---|
Un solo <h1> y títulos en orden (h1 → h2 → h3) |
Busca <h1 con Ctrl + F en el código |
Landmarks: <header>, <nav>, <main>, <footer> |
Revisa que estén presentes y que no falten |
Teclado: todo se puede usar solo con Tab, Enter y Esc |
Deja el mouse quieto y recorre el sitio |
| Foco visible: al tabular se ve dónde estás | Bootstrap lo trae; no lo quites con outline: none |
Textos alternativos: cada foto con alt="descripción real", y los emojis decorativos con role="img" + aria-label |
Las fotos que agregaste en el módulo 2 y los seis emojis de la galería |
| Foco al enviar un formulario: el foco va al primer error o al mensaje de éxito | Envía el formulario vacío y presiona Tab: el foco debe estar en el campo marcado |
Anclas: ningún enlace apunta a un id inexistente |
Haz clic en cada enlace del menú y en "Volver arriba" |
| Sin desbordes: nada obliga a desplazar la página de lado a lado | A 375 px no debe aparecer barra horizontal |
Etiquetas en formularios: todo campo tiene su <label> |
Haz clic en una etiqueta: debe enfocar su campo |
| Contraste de colores: texto legible sobre su fondo | Usa WebAIM Contrast Checker |
Movimiento: nada se mueve solo; se respeta prefers-reduced-motion |
Ya lo hicimos en el CSS |
| Enlace "Saltar al contenido" | Presiona Tab al cargar |
3. Prueba el diseño responsive. En las herramientas del navegador, comprueba estos anchos: 375 px (celular), 768 px (tablet), 1280 px (laptop). En cada uno confirma que: no hay scroll horizontal, nada se corta, los botones se pueden tocar con facilidad y los textos se leen sin hacer zoom.
4. Haz una auditoría automática. En Chrome o Edge: F12 → pestaña Lighthouse → marca Accessibility y Best Practices → Analyze page load. Lee las sugerencias. No necesitas 100 puntos, pero entender cada aviso te enseña mucho.
5. Valida tu HTML pegándolo en el validador del W3C. Corrige lo que marque como error.
#Buenas prácticas para tus propios proyectos
- Usa utilidades primero, y CSS propio solo cuando no exista una utilidad.
- No modifiques los archivos de Bootstrap. Siempre sobrescribe en tu propio CSS.
- Evita
!important. Si lo necesitas mucho, tu CSS está mal ordenado o es muy débil. - Mantén el orden: primero el CSS de Bootstrap, luego el tuyo; primero el JS de Bootstrap, luego el tuyo.
- Usa HTML semántico (
<section>,<nav>,<footer>) y no solo<div>. - Piensa primero en el celular y luego agrega
md,lg, etc. - Nombra tus clases propias con sentido (
.hero-card,.text-gradient). - Un
idsolo una vez por página. - Consulta la documentación de cada componente: cada una trae el HTML mínimo y las variantes.
#Resultado esperado
El sitio completo: navbar, hero, estadísticas, tarjetas, galería, carrusel, acordeón, formulario, modal y pie de página, con modo claro y oscuro. Sin errores en la consola.
#Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| Hay scroll horizontal en celular | Casi siempre hay dos causas: un g-5 dentro de un .container, o un texto largo sin partir |
Con F12 selecciona el elemento culpable: si es una .row, cambia a g-4 g-lg-5; si es un texto (correo, URL), agrega overflow-wrap: anywhere al body |
| La barra horizontal aparece solo en celular | Estás viendo el gutter grande en pantallas chicas | g-4 en móvil y sube a g-5 con g-lg-5 |
| El sitio parpadea al abrir | El tema se aplica al final del JS | Mueve el script del tema al <head> (módulo 1) |
| El pie queda a mitad de pantalla en páginas cortas | Poco contenido | Es normal en este sitio; para pegarlo abajo puedes usar d-flex flex-column min-vh-100 en el <body> |
| Lighthouse reporta bajo contraste | Texto gris sobre fondo oscuro | Usa text-body-secondary o aclara el color |
Los id están duplicados |
Copiaste bloques sin cambiarlos | Cámbialos y actualiza sus referencias |
#Mini-ejercicio
Ejecuta Lighthouse en modo claro y en modo oscuro. Escribe el número de accesibilidad de cada uno y elige un aviso para corregir.
💡 Pista
Cambia de modo con el botón de la navbar y vuelve a analizar. Para entender un aviso, haz clic sobre él: Lighthouse te muestra el elemento y un enlace con la explicación.#✅ Punto de control
- [ ] El pie de página está agregado y se ve bien en celular y escritorio.
- [ ] Recorrí todo el sitio usando solo el teclado.
- [ ] Probé el sitio en 375, 768 y 1280 px sin scroll horizontal.
- [ ] Ejecuté Lighthouse y leí sus sugerencias.
- [ ] La consola del navegador no muestra errores.
- [ ] En el formulario, el foco salta al primer error y luego al mensaje de éxito.
- [ ] Borré la preferencia de tema guardada y el sitio arrancó con el tema de mi sistema.
#Módulo 10: Tu reto final y siguientes pasos
⏱ 10 minutos de planificación y primer paso. El reto completo te tomará entre 30 y 60 minutos adicionales a tu ritmo.
🎯 Objetivo: pasar de "seguir instrucciones" a "tomar decisiones". Aquí no hay código para copiar.
#Reto 1: amplía PixelCrew
Agrega una nueva sección "Calendario de torneos" a tu sitio, sin copiar código del tutorial. Debe cumplir:
- [ ] Estar entre las secciones "Galería" y "Comunidad", con su enlace en la navbar.
- [ ] Usar al menos 3 componentes que no hayas usado en el sitio. Opciones: tabla (
table,table-striped) dentro detable-responsive, pestañas (nav-tabscontab-content), lista de grupo (list-group), barra de progreso (progress), tooltip o toast. - [ ] Verse bien en 375, 768 y 1280 px.
- [ ] Funcionar en modo claro y oscuro.
- [ ] Respetar las reglas de accesibilidad del módulo 9.
💡 Busca cada componente en la documentación oficial: copia su HTML mínimo y adáptalo. Leer la documentación y adaptar ejemplos es exactamente cómo trabajan los profesionales.
#Reto 2: tu propio proyecto
Elige algo que te importe y que quieras construir. Ideas:
| Idea | Secciones sugeridas |
|---|---|
| Portafolio personal | Presentación, proyectos (tarjetas), contacto |
| Sitio de tu banda o equipo | Integrantes, próximos eventos, galería |
| Página de un evento del colegio | Programa, ubicación, inscripción |
| Mini tienda de un emprendimiento | Productos (tarjetas), preguntas, contacto |
| Fan site de un hobby (anime, deportes, música) | Noticias, galería, comunidad |
Plan de 10 minutos (haz esto ahora):
- Dibuja en papel cómo se vería en celular: bloques de arriba hacia abajo.
- Lista las secciones y al lado escribe qué componente de Bootstrap usarías para cada una (usa la tabla de abajo).
- Crea el esqueleto: copia la plantilla del módulo 1, cambia el título y deja los marcadores.
- Construye la primera sección y pruébala en el celular (modo responsive) antes de seguir.
¿Qué componente uso para...?
| Necesito... | Uso... |
|---|---|
| Menú de navegación | navbar |
| Un bloque de portada | container + utilidades (como el hero) |
| Mostrar productos, personas o proyectos | card dentro de un grid |
| Mostrar varias fotos en fila | row-cols-* + ratio o card |
| Preguntas y respuestas | accordion |
| Información que aparece sobre la página | modal u offcanvas |
| Mensajes de aviso | alert o toast |
| Datos ordenados | table dentro de table-responsive |
| Cambiar entre contenidos en el mismo espacio | nav-tabs o nav-pills |
| Recoger datos del usuario | form + validación |
#Siguientes pasos
Cuando te sientas cómodo/a con Bootstrap, estas son las rutas naturales para crecer:
- Íconos: Bootstrap Icons (más de 2000 íconos gratis).
- JavaScript: aprende lo básico del lenguaje para dar más vida a tus sitios (el módulo 6 y el 8 te dieron una probada).
- Git y GitHub: guarda tu código y publica tu sitio gratis con GitHub Pages.
- Herramientas modernas: npm, Sass y un empaquetador como Vite.
- Otras ideas de diseño: estudia las plantillas de ejemplo de Bootstrap para ver cómo se combinan los componentes.
#Código completo del proyecto
Compara tu trabajo con la versión final. Si algo falla, busca la diferencia con tu archivo.
#index.html
<!DOCTYPE html>
<html lang="es" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="PixelCrew: el club de videojuegos de tu colegio. Torneos, noches de juego y talleres.">
<title>PixelCrew | Club de videojuegos</title>
<!-- Aplica el tema ANTES de que se vea la página (evita el parpadeo).
Va primero en el <head> a propósito; en un sitio real se le llama
"anti-flash" y casi siempre se escribe en una sola línea. -->
<script>
(function () {
try {
var guardado = localStorage.getItem('pc-tema');
var oscuroDelSistema = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-bs-theme', guardado || (oscuroDelSistema ? 'dark' : 'light'));
} catch (error) {
document.documentElement.setAttribute('data-bs-theme', 'light');
}
})();
</script>
<!-- Bootstrap 5.3.8 (CSS) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<!-- Tu CSS (siempre DESPUÉS de Bootstrap) -->
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- NAVBAR -->
<a class="visually-hidden-focusable" href="#contenido">Saltar al contenido</a>
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom sticky-top" aria-label="Navegación principal">
<div class="container">
<a class="navbar-brand fw-bold" href="#inicio">🎮 PixelCrew</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Abrir o cerrar el menú">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menuPrincipal">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0 align-items-lg-center">
<li class="nav-item"><a class="nav-link" href="#juegos">Qué hacemos</a></li>
<li class="nav-item"><a class="nav-link" href="#galeria">Galería</a></li>
<li class="nav-item"><a class="nav-link" href="#testimonios">Comunidad</a></li>
<li class="nav-item"><a class="nav-link" href="#faq">Preguntas</a></li>
<li class="nav-item ms-lg-3 my-2 my-lg-0"><a class="btn btn-primary btn-sm" href="#unete">Únete</a></li>
<li class="nav-item ms-lg-2 mb-2 mb-lg-0">
<button id="themeToggle" class="btn btn-outline-secondary btn-sm" type="button" aria-label="Cambiar entre modo claro y modo oscuro">
<span id="themeIcon" aria-hidden="true">☀️</span>
</button>
</li>
</ul>
</div>
</div>
</nav>
</header>
<main id="contenido">
<!-- HERO -->
<section id="inicio" class="hero py-5">
<div class="container py-lg-5">
<div class="row align-items-center g-4 g-lg-5">
<div class="col-lg-7">
<p class="text-uppercase fw-semibold text-primary-emphasis small mb-2">Club de videojuegos del colegio</p>
<h1 class="display-3 fw-bold mb-3">Juega, crea y compite con <span class="text-gradient">PixelCrew</span></h1>
<p class="lead text-body-secondary mb-4">
Somos el club donde los videojuegos se juegan, se estudian y hasta se programan.
Si tienes entre 14 y 18 años, hay un lugar para ti en el equipo.
</p>
<div class="d-flex flex-column flex-sm-row gap-3">
<a href="#unete" class="btn btn-primary btn-lg px-4">Únete al club</a>
<a href="#juegos" class="btn btn-outline-secondary btn-lg px-4">Descubre qué hacemos</a>
</div>
</div>
<div class="col-lg-5 d-none d-lg-block">
<div class="hero-card rounded-4 shadow-lg p-5 text-center text-white" aria-hidden="true">
<div class="display-1">🎮</div>
<p class="fs-4 fw-semibold mb-0">Nivel 1: ¡empieza hoy!</p>
</div>
</div>
</div>
</div>
</section>
<!-- ESTADÍSTICAS -->
<section class="py-4 border-top border-bottom bg-body-tertiary" aria-label="El club en números">
<div class="container">
<div class="row g-3 text-center">
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">120+</p>
<p class="text-body-secondary mb-0">miembros</p>
</div>
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">12</p>
<p class="text-body-secondary mb-0">torneos al año</p>
</div>
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">3</p>
<p class="text-body-secondary mb-0">talleres semanales</p>
</div>
<div class="col-6 col-md-3">
<p class="display-6 fw-bold text-primary-emphasis mb-0">1</p>
<p class="text-body-secondary mb-0">sala gamer</p>
</div>
</div>
</div>
</section>
<!-- QUÉ HACEMOS -->
<section id="juegos" class="py-5">
<div class="container">
<div class="alert alert-info alert-dismissible fade show" role="alert">
<strong>¡Inscripciones abiertas!</strong> Hay cupos disponibles hasta el 31 de octubre.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar aviso"></button>
</div>
<div class="row justify-content-center my-5">
<div class="col-lg-7 text-center">
<h2 class="mb-2">Más que jugar</h2>
<p class="text-body-secondary mb-0">Tres formas de formar parte del club. Elige la tuya (o prueba las tres).</p>
</div>
</div>
<div class="row row-cols-1 row-cols-md-3 g-4">
<div class="col">
<div class="card h-100 card-hover shadow-sm">
<div class="card-body">
<span class="badge text-bg-warning mb-3">Cada viernes</span>
<h3 class="card-title h5">🏆 Torneos</h3>
<p class="card-text text-body-secondary">Compite en equipo o en solitario en juegos para todos los niveles. Hay premios, diplomas y mucha rivalidad sana.</p>
</div>
<div class="card-footer bg-transparent border-0 pb-3">
<a href="#unete" class="btn btn-outline-primary w-100">Quiero competir</a>
</div>
</div>
</div>
<div class="col">
<div class="card h-100 card-hover shadow-sm">
<div class="card-body">
<span class="badge text-bg-success mb-3">Cada miércoles</span>
<h3 class="card-title h5">🕹️ Noches de juego</h3>
<p class="card-text text-body-secondary">Juegos cooperativos y de mesa, música y snacks. Ideal para conocer gente nueva sin presión.</p>
</div>
<div class="card-footer bg-transparent border-0 pb-3">
<a href="#unete" class="btn btn-outline-primary w-100">Me apunto</a>
</div>
</div>
</div>
<div class="col">
<div class="card h-100 card-hover shadow-sm">
<div class="card-body">
<span class="badge text-bg-info mb-3">Cada martes</span>
<h3 class="card-title h5">💻 Taller de creación</h3>
<p class="card-text text-body-secondary">Aprende a diseñar niveles y personajes, y a programar tu primer videojuego desde cero.</p>
</div>
<div class="card-footer bg-transparent border-0 pb-3">
<a href="#unete" class="btn btn-outline-primary w-100">Quiero crear</a>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- GALERÍA -->
<section id="galeria" class="py-5">
<div class="container">
<h2 class="text-center mb-2">Momentos del club</h2>
<p class="text-center text-body-secondary mb-4">Un vistazo a lo que pasa en las reuniones. (Aquí irían tus fotos reales.)</p>
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-primary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un trofeo">🏆</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-success d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un joystick">🕹️</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-danger d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de unos audífonos">🎧</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-warning d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un computador">💻</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-info d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un dado">🎲</div>
</div>
</div>
<div class="col">
<div class="ratio ratio-1x1">
<div class="rounded-4 bg-secondary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un apretón de manos">🤝</div>
</div>
</div>
</div>
</div>
</section>
<!-- COMUNIDAD -->
<section id="testimonios" class="py-5 bg-body-tertiary">
<div class="container">
<h2 class="text-center mb-4">Lo que dice la comunidad</h2>
<div class="row justify-content-center">
<div class="col-lg-8">
<div id="carruselComunidad" class="carousel slide" aria-label="Testimonios de miembros del club">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Testimonio 1"></button>
<button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="1" aria-label="Testimonio 2"></button>
<button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="2" aria-label="Testimonio 3"></button>
</div>
<div class="carousel-inner rounded-4 shadow">
<div class="carousel-item active">
<figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
<blockquote class="fs-4 mb-3">“Entré por los torneos y salí sabiendo hacer mi primer videojuego.”</blockquote>
<figcaption class="fw-semibold">Valentina, 16 años</figcaption>
</figure>
</div>
<div class="carousel-item">
<figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
<blockquote class="fs-4 mb-3">“Aquí conocí a mis mejores amigos. Jugar en equipo es otro nivel.”</blockquote>
<figcaption class="fw-semibold">Santiago, 17 años</figcaption>
</figure>
</div>
<div class="carousel-item">
<figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
<blockquote class="fs-4 mb-3">“Pensé que era solo jugar, pero también aprendí a organizar eventos.”</blockquote>
<figcaption class="fw-semibold">Camila, 15 años</figcaption>
</figure>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carruselComunidad" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Anterior</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carruselComunidad" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Siguiente</span>
</button>
</div>
<p class="text-center text-body-secondary small mt-3 mb-0">Los nombres de este ejemplo son ficticios.</p>
</div>
</div>
</div>
</section>
<!-- PREGUNTAS -->
<section id="faq" class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<h2 class="text-center mb-4">Preguntas frecuentes</h2>
<div class="accordion" id="acordeonFaq">
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1" aria-expanded="true" aria-controls="faq1">
¿Necesito experiencia para entrar?
</button>
</h3>
<div id="faq1" class="accordion-collapse collapse show" data-bs-parent="#acordeonFaq">
<div class="accordion-body">Para nada. Hay miembros que juegan desde hace años y otros que nunca han tenido una consola. Te ayudamos a empezar.</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2" aria-expanded="false" aria-controls="faq2">
¿Cuánto cuesta?
</button>
</h3>
<div id="faq2" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
<div class="accordion-body">Unirte es gratis. Solo pedimos que cuides el material y participes en las actividades.</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3" aria-expanded="false" aria-controls="faq3">
¿Cuándo nos reunimos?
</button>
</h3>
<div id="faq3" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
<div class="accordion-body">Los martes, miércoles y viernes después de clases. Te enviamos el calendario completo cuando te inscribas.</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4" aria-expanded="false" aria-controls="faq4">
¿Necesito mi propio equipo?
</button>
</h3>
<div id="faq4" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
<div class="accordion-body">No. El club tiene consolas, computadores y mandos para todos. Si quieres traer el tuyo, ¡bienvenido!</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ÚNETE -->
<section id="unete" class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-7">
<h2 class="text-center mb-2">Únete a PixelCrew</h2>
<p class="text-center text-body-secondary mb-4">Completa el formulario y te contamos cuándo es la primera reunión.</p>
<div id="exito" class="alert alert-success d-none" role="status" tabindex="-1">
🎉 ¡Listo! Recibimos tu inscripción. Nos vemos en el siguiente nivel.
</div>
<form id="formUnete" class="row g-3" novalidate>
<div class="col-md-6">
<div class="form-floating">
<input type="text" class="form-control" id="nombre" placeholder="Tu nombre" required minlength="2" autocomplete="given-name">
<label for="nombre">Nombre</label>
<div class="invalid-feedback">Escribe tu nombre (mínimo 2 letras).</div>
</div>
</div>
<div class="col-md-6">
<div class="form-floating">
<input type="email" class="form-control" id="correo" placeholder="tucorreo@ejemplo.com" required autocomplete="email">
<label for="correo">Correo electrónico</label>
<div class="invalid-feedback">Escribe un correo válido, por ejemplo nombre@correo.com.</div>
</div>
</div>
<div class="col-md-4">
<div class="form-floating">
<input type="number" class="form-control" id="edad" placeholder="16" required min="13" max="18">
<label for="edad">Edad</label>
<div class="invalid-feedback">El club es para edades entre 13 y 18 años.</div>
</div>
</div>
<div class="col-md-8">
<div class="form-floating">
<select class="form-select" id="interes" required>
<option value="" selected disabled>Elige una opción…</option>
<option>Torneos</option>
<option>Noches de juego</option>
<option>Taller de creación</option>
<option>Todo lo anterior</option>
</select>
<label for="interes">¿Qué te interesa más?</label>
<div class="invalid-feedback">Elige una opción de la lista.</div>
</div>
</div>
<div class="col-12">
<div class="input-group">
<span class="input-group-text" id="arroba">@</span>
<input type="text" class="form-control" id="usuario" placeholder="Tu usuario gamer (opcional)" aria-label="Tu usuario gamer (opcional)" aria-describedby="arroba">
</div>
</div>
<div class="col-12">
<label for="mensaje" class="form-label">Cuéntanos tu juego favorito (opcional)</label>
<textarea class="form-control" id="mensaje" rows="3"></textarea>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="reglas" required>
<label class="form-check-label" for="reglas">
Acepto las
<button type="button" class="btn btn-link p-0 align-baseline" data-bs-toggle="modal" data-bs-target="#modalReglas">reglas del club</button>
</label>
<div class="invalid-feedback">Debes aceptar las reglas para inscribirte.</div>
</div>
</div>
<div class="col-12 d-grid">
<button class="btn btn-primary btn-lg" type="submit">Enviar inscripción</button>
</div>
</form>
</div>
</div>
</div>
</section>
</main>
<!-- FOOTER -->
<footer class="border-top py-4 mt-5">
<div class="container d-flex flex-column flex-md-row justify-content-between align-items-center gap-3">
<p class="mb-0 text-body-secondary small">© 2026 PixelCrew · Hecho con Bootstrap 5.3.8</p>
<ul class="nav">
<li class="nav-item"><a class="nav-link px-2" href="#juegos">Qué hacemos</a></li>
<li class="nav-item"><a class="nav-link px-2" href="#faq">Preguntas</a></li>
<li class="nav-item"><a class="nav-link px-2" href="#unete">Únete</a></li>
</ul>
<a href="#inicio" class="btn btn-outline-secondary btn-sm">Volver arriba ↑</a>
</div>
</footer>
<!-- MODAL -->
<div class="modal fade" id="modalReglas" tabindex="-1" aria-labelledby="tituloReglas" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="tituloReglas">📜 Reglas de PixelCrew</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<ol class="mb-0">
<li class="mb-2"><strong>Respeto ante todo.</strong> Nada de insultos ni burlas, ni dentro ni fuera del juego.</li>
<li class="mb-2"><strong>Juego limpio.</strong> Sin trampas, hacks ni cuentas prestadas en los torneos.</li>
<li class="mb-2"><strong>Equilibrio.</strong> Las tareas del colegio van primero; el club puede esperar.</li>
<li class="mb-2"><strong>Cuidamos el material.</strong> Consolas, mandos y equipos son de todos.</li>
<li class="mb-2"><strong>Contenido apto.</strong> Los juegos deben ser adecuados para la edad de todos.</li>
<li><strong>Si algo te incomoda</strong>, avísale a una persona responsable del club. Siempre te vamos a escuchar.</li>
</ol>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Entendido</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5.3.8 (JS con Popper incluido) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<!-- Tu JavaScript (después de Bootstrap) -->
<script src="js/main.js"></script>
</body>
</html>
#css/style.css
/* ===== 1. Variables: personalizamos Bootstrap sin tocar sus archivos ===== */
:root {
--pc-accent: #7c4dff;
/* Variables de Bootstrap que sobrescribimos */
--bs-primary: #7c4dff;
--bs-primary-rgb: 124, 77, 255;
--bs-primary-text-emphasis: #4a22b8;
--bs-primary-bg-subtle: #e9e0ff;
--bs-primary-border-subtle: #cbb8ff;
--bs-border-radius: 0.75rem;
}
/* Este bloque va DESPUÉS de :root para ganar en modo oscuro */
[data-bs-theme="dark"] {
--bs-body-bg: #0e0f1f;
--bs-body-bg-rgb: 14, 15, 31;
--bs-tertiary-bg: #161833;
--bs-tertiary-bg-rgb: 22, 24, 51;
--bs-primary-text-emphasis: #b8a0ff;
--bs-primary-bg-subtle: #241a4d;
--bs-primary-border-subtle: #4a2fa8;
}
/* Los componentes de Bootstrap guardan sus colores en variables LOCALES.
Las cambiamos para que los botones también usen nuestro morado. */
.btn-primary {
--bs-btn-bg: var(--pc-accent);
--bs-btn-border-color: var(--pc-accent);
--bs-btn-hover-bg: #6a3de8;
--bs-btn-hover-border-color: #6a3de8;
--bs-btn-active-bg: #5a30d0;
--bs-btn-active-border-color: #5a30d0;
--bs-btn-disabled-bg: var(--pc-accent);
--bs-btn-disabled-border-color: var(--pc-accent);
--bs-btn-focus-shadow-rgb: 124, 77, 255;
}
.btn-outline-primary {
--bs-btn-color: var(--bs-primary-text-emphasis);
--bs-btn-border-color: var(--pc-accent);
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: var(--pc-accent);
--bs-btn-hover-border-color: var(--pc-accent);
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #5a30d0;
--bs-btn-active-border-color: #5a30d0;
--bs-btn-focus-shadow-rgb: 124, 77, 255;
}
.card {
--bs-card-bg: var(--bs-tertiary-bg);
}
/* ===== 2. Ajustes generales ===== */
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
/* Evita que la navbar fija tape los títulos al navegar por el menú */
section[id] { scroll-margin-top: 4.5rem; }
/* Un texto muy largo (un correo, una URL) puede empujar la página y crear
scroll horizontal en celular. `overflow-wrap: anywhere` lo parte.
Ojo: `anywhere`, no `break-word`, porque es el único que además reduce el
ancho mínimo de las celdas de un grid. */
body {
overflow-wrap: anywhere;
}
/* ===== 3. Hero ===== */
.hero {
background:
radial-gradient(circle at 15% 20%, rgba(124, 77, 255, 0.25), transparent 45%),
radial-gradient(circle at 85% 80%, rgba(13, 139, 217, 0.18), transparent 45%);
}
.text-gradient {
background: linear-gradient(90deg, #7c4dff, #0d8bd9);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero-card {
background: linear-gradient(135deg, #7c4dff, #0d8bd9);
}
/* ===== 4. Tarjetas ===== */
.card-hover {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card-hover:hover {
transform: translateY(-6px);
box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.25) !important;
}
@media (prefers-reduced-motion: reduce) {
.card-hover { transition: none; }
.card-hover:hover { transform: none; }
}
/* ===== 5. Carrusel de la comunidad ===== */
.testimonio {
min-height: 280px;
background: linear-gradient(135deg, #4b2bb5, #0b6fa8);
}
/* En modo oscuro Bootstrap pone controles negros en los carruseles.
Como nuestras diapositivas son oscuras en ambos modos, los forzamos a blanco. */
.carousel {
--bs-carousel-indicator-active-bg: #fff;
--bs-carousel-caption-color: #fff;
--bs-carousel-control-icon-filter: none;
}
#js/main.js
// ===== Validación del formulario de inscripción =====
const formulario = document.getElementById('formUnete');
const mensajeExito = document.getElementById('exito');
formulario.addEventListener('submit', (evento) => {
evento.preventDefault(); // este sitio de práctica no envía datos a ningún servidor
if (formulario.checkValidity()) {
formulario.reset();
formulario.classList.remove('was-validated');
mensajeExito.classList.remove('d-none');
mensajeExito.focus(); // el foco va al mensaje: quien navega con
// teclado o lector de pantalla se entera
} else {
mensajeExito.classList.add('d-none');
formulario.classList.add('was-validated');
// Nadie se da cuenta de un error si no lo ve. Movemos el foco al primer
// campo inválido: el navegador lo resalta y además lo deja preparado
// para escribir de nuevo.
const primerError = formulario.querySelector(':invalid');
if (primerError) primerError.focus();
}
});
// ===== Modo claro / oscuro =====
const raiz = document.documentElement;
const botonTema = document.getElementById('themeToggle');
const iconoTema = document.getElementById('themeIcon');
const CLAVE = 'pc-tema'; // el nombre con el que guardamos la preferencia
// El tema que ya tiene el dispositivo. Se consulta, no se supone.
const consultaSistema = window.matchMedia('(prefers-color-scheme: dark)');
// Leer y guardar puede fallar en modo privado: por eso van protegidos.
function leerTemaGuardado() {
try {
return localStorage.getItem(CLAVE);
} catch (error) {
return null;
}
}
function aplicarTema(tema, recordar) {
raiz.setAttribute('data-bs-theme', tema);
iconoTema.textContent = tema === 'dark' ? '☀️' : '🌙'; // el ícono muestra a qué modo cambiarás
// Solo guardamos cuando la ELECCIÓN es de la persona. Si guardáramos el
// tema del sistema, después no sabríamos si ella eligió o si fue el default.
if (recordar) {
try {
localStorage.setItem(CLAVE, tema);
} catch (error) {
/* en modo privado no se guarda, pero el sitio funciona igual */
}
}
}
// Al cargar: primero la elección de la persona; si no hay, la del sistema.
aplicarTema(leerTemaGuardado() || (consultaSistema.matches ? 'dark' : 'light'), false);
botonTema.addEventListener('click', () => {
const actual = raiz.getAttribute('data-bs-theme');
aplicarTema(actual === 'dark' ? 'light' : 'dark', true);
});
// Si la persona NO ha elegido nada y cambia el tema del sistema, la seguimos.
consultaSistema.addEventListener('change', (evento) => {
if (!leerTemaGuardado()) {
aplicarTema(evento.matches ? 'dark' : 'light', false);
}
});
#Hoja de referencia rápida
#Grid y contenedores
| Clase | Efecto |
|---|---|
container, container-fluid, container-lg |
Contenedor centrado / a todo el ancho / fluido hasta lg |
row |
Fila |
col, col-6, col-md-4, col-lg-auto |
Columna: igual reparto / 6 de 12 / 4 desde md / según contenido |
row-cols-1, row-cols-md-3 |
Cuántas columnas por fila |
g-3, gx-2, gy-4 |
Separación entre columnas |
offset-md-2 |
Deja espacio vacío a la izquierda |
#Espaciado
| Clase | Efecto |
|---|---|
m-*, p-* |
Margen / relleno (0–5, auto) |
mt-*, mb-*, ms-*, me-* |
Arriba, abajo, inicio, final |
mx-*, my-*, px-*, py-* |
Horizontal / vertical |
gap-* |
Separación entre elementos en flex o grid |
#Texto y color
| Clase | Efecto |
|---|---|
display-1…display-6, h1…h6, lead, small |
Tamaños de texto |
fs-1…fs-6 |
Tamaño de fuente |
fw-bold, fw-semibold, fw-normal, fst-italic |
Peso y estilo |
text-start, text-center, text-end |
Alineación |
text-uppercase, text-truncate |
Transformación del texto |
text-primary, text-body-secondary, text-white |
Color de texto |
bg-primary, bg-body-tertiary, bg-success-subtle |
Color de fondo |
text-bg-primary |
Fondo y texto con buen contraste a la vez |
#Display y flexbox
| Clase | Efecto |
|---|---|
d-none, d-block, d-flex, d-lg-none |
Mostrar y ocultar (con breakpoints) |
flex-row, flex-column, flex-wrap |
Dirección y salto de línea |
justify-content-center, -between, -end |
Alineación en el eje principal |
align-items-center, -start, -end |
Alineación en el eje cruzado |
ms-auto, me-auto |
Empuja hacia un lado |
#Bordes, sombras y tamaños
| Clase | Efecto |
|---|---|
border, border-top, border-0 |
Bordes |
rounded, rounded-3, rounded-pill, rounded-circle |
Esquinas |
shadow-sm, shadow, shadow-lg |
Sombras |
w-100, h-100, mw-100, min-vh-100 |
Anchos y altos |
ratio ratio-16x9 |
Proporción fija |
position-relative, sticky-top, fixed-bottom |
Posición |
visually-hidden, visually-hidden-focusable |
Oculto visualmente pero accesible |
#Componentes
| Componente | Estructura mínima |
|---|---|
| Botón | btn btn-primary, btn-outline-*, btn-lg, btn-sm |
| Badge | badge text-bg-primary |
| Alerta | alert alert-success (+ alert-dismissible) |
| Tarjeta | card > card-body > card-title, card-text |
| Navbar | navbar navbar-expand-lg > navbar-brand, navbar-toggler, navbar-collapse |
| Formulario | form-control, form-select, form-check, form-floating, invalid-feedback |
| Lista de grupo | list-group > list-group-item |
| Tabla | table table-striped dentro de table-responsive |
| Barra de progreso | progress > progress-bar |
| Modal | modal > modal-dialog > modal-content > modal-header/body/footer |
| Acordeón | accordion > accordion-item > accordion-header, accordion-collapse |
| Carrusel | carousel > carousel-inner > carousel-item |
| Pestañas | nav nav-tabs + tab-content > tab-pane |
#Atributos de JavaScript
| Atributo | Uso |
|---|---|
data-bs-toggle |
collapse, modal, tab, dropdown, tooltip, offcanvas |
data-bs-target |
Selector (#id) del elemento afectado |
data-bs-dismiss |
modal, alert, toast, offcanvas |
data-bs-slide, data-bs-slide-to |
Carrusel |
data-bs-parent |
Acordeón de uno a la vez |
data-bs-theme |
light o dark en cualquier elemento |
#De Bootstrap 4 a Bootstrap 5
| Bootstrap 4 | Bootstrap 5 |
|---|---|
ml-*, mr-*, pl-*, pr-* |
ms-*, me-*, ps-*, pe-* |
text-left, text-right |
text-start, text-end |
float-left, float-right |
float-start, float-end |
font-weight-bold |
fw-bold |
data-toggle, data-target, data-dismiss |
data-bs-toggle, data-bs-target, data-bs-dismiss |
form-group, form-row |
mb-3, row g-3 |
badge badge-primary |
badge text-bg-primary |
btn-block |
d-grid en el contenedor, o w-100 |
sr-only |
visually-hidden |
text-muted |
text-body-secondary |
jQuery |
No es necesario: Bootstrap 5 funciona con JavaScript puro |
#Retos adicionales y recursos oficiales
#Retos adicionales
Cuando termines, prueba cualquiera de estos (están de menor a mayor dificultad):
- Tooltips: agrega un tooltip a los emojis de la galería. (Necesitas inicializarlos con JavaScript: busca "Tooltips" en la documentación.)
- Dropdown: convierte "Qué hacemos" en la navbar en un menú desplegable con los tres tipos de actividad.
- Toast: al enviar el formulario con éxito, muestra un toast en una esquina en vez de la alerta verde.
- Offcanvas: crea un panel lateral con las reglas del club en vez del modal.
- Scrollspy: resalta en la navbar la sección que estás viendo. (Busca "Scrollspy".)
- Un tercer tema: en lugar de solo claro y oscuro, crea una clase
.tema-retrocon tus propias variables y alterna entre tres modos. - Sass real: monta el proyecto con npm y Sass, con una paleta propia de 3 colores.
- Publica tu sitio: súbelo a GitHub Pages y compártelo con tus amistades.
#Recursos oficiales
| Recurso | Para qué |
|---|---|
| Documentación de Bootstrap 5.3 | La referencia definitiva, con ejemplos de cada componente |
| Ejemplos oficiales | Plantillas completas para inspirarte |
| Bootstrap Icons | Íconos gratuitos para tus proyectos |
| MDN Web Docs (español) | HTML, CSS y JavaScript explicados con detalle |
| WCAG: guía rápida de accesibilidad | Criterios de accesibilidad web |
| WebAIM Contrast Checker | Comprueba el contraste de colores |
| Validador HTML del W3C | Encuentra errores en tu HTML |
#Lo que lograste
Empezaste con una página vacía y hoy sabes: organizar contenido con el grid, dar estilo con utilidades, armar componentes, validar formularios, usar el JavaScript de Bootstrap, personalizar colores y temas, y revisar la accesibilidad de un sitio. Eso es una base real de desarrollo front-end.
La mejor forma de consolidarlo es construir algo que te importe. Empieza hoy con el Reto 2. 🚀