Módulo 16: Bootstrap: componentes

Objetivos del módulo

  • Entender cómo funcionan los componentes de Bootstrap (clases + atributos data-bs-* + JavaScript)
  • Crear botones, alertas, badges, cards, listas y tablas
  • Construir una navbar responsive con menú hamburguesa y desplegables
  • Usar navegación por pestañas, breadcrumb y paginación
  • Implementar componentes interactivos: dropdown, modal, offcanvas, carrusel, acordeón, collapse y toasts
  • Inicializar con JavaScript los tooltips y popovers
  • Indicar estados de carga con spinners, placeholders y barras de progreso
  • Maquetar y validar formularios con Bootstrap

1. Cómo funcionan los componentes

📘 Concepto: Un componente de Bootstrap es un bloque de interfaz reutilizable formado por una estructura HTML concreta y unas clases. Los componentes interactivos (menús, modales, carruseles…) necesitan además el JavaScript de Bootstrap, que se activa mediante atributos data-bs-*.

Piensa en los componentes como en piezas de LEGO con instrucciones: si respetas la estructura de la documentación (qué elemento va dentro de cuál y con qué clase), la pieza funciona. Si te saltas un paso, falla.

Tipo Ejemplos ¿Necesita JS?
Solo CSS Botones, badges, cards, list group, tablas, spinners, progress, breadcrumb No
Interactivos con data-bs-* Navbar (toggler), dropdown, modal, offcanvas, carrusel, acordeón, collapse, tabs, alertas descartables Sí, se activan solos
Interactivos que hay que inicializar Tooltips, popovers, toasts Sí, y con código JS propio

Todos los ejemplos de este módulo suponen que tu página usa la plantilla del módulo anterior, con el CSS en el <head> y bootstrap.bundle.min.js al final del <body>.

Atributo Para qué sirve
data-bs-toggle="..." Qué componente activa este elemento (collapse, modal, dropdown, tab, offcanvas…)
data-bs-target="#id" Sobre qué elemento actúa
data-bs-dismiss="..." Cierra el componente que lo contiene (modal, alert, toast, offcanvas)

⚠️ Importante: Los atributos aria-* (aria-expanded, aria-controls, aria-label…) que verás en los ejemplos no son decorativos: hacen que el componente sea accesible para lectores de pantalla. Cópialos siempre.


2. Botones y grupos de botones

<!-- Variantes de color -->
<button type="button" class="btn btn-primary">Primario</button>
<button type="button" class="btn btn-secondary">Secundario</button>
<button type="button" class="btn btn-success">Éxito</button>
<button type="button" class="btn btn-danger">Peligro</button>
<button type="button" class="btn btn-link">Enlace</button>

<!-- Contorno, tamaños y estado -->
<button type="button" class="btn btn-outline-primary">Contorno</button>
<button type="button" class="btn btn-primary btn-lg">Grande</button>
<button type="button" class="btn btn-primary btn-sm">Pequeño</button>
<button type="button" class="btn btn-primary" disabled>Deshabilitado</button>

<!-- Un enlace con aspecto de botón -->
<a href="contacto.html" class="btn btn-success" role="button">Contactar</a>

<!-- Botones a todo el ancho en móvil y en línea desde md -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
  <button class="btn btn-outline-secondary" type="button">Cancelar</button>
  <button class="btn btn-primary" type="button">Guardar</button>
</div>

Grupos y botones conmutables

<!-- Grupo de botones -->
<div class="btn-group" role="group" aria-label="Alineación del texto">
  <button type="button" class="btn btn-outline-primary">Izquierda</button>
  <button type="button" class="btn btn-outline-primary">Centro</button>
  <button type="button" class="btn btn-outline-primary">Derecha</button>
</div>

<!-- Selector tipo "segmented control" con radios reales (accesible) -->
<div class="btn-group" role="group" aria-label="Periodo de facturación">
  <input type="radio" class="btn-check" name="periodo" id="mensual" autocomplete="off" checked>
  <label class="btn btn-outline-primary" for="mensual">Mensual</label>

  <input type="radio" class="btn-check" name="periodo" id="anual" autocomplete="off">
  <label class="btn btn-outline-primary" for="anual">Anual</label>
</div>

💡 Consejo: Usa <button> para acciones (enviar, abrir, cerrar) y <a> para navegar a otra página. La clase .btn solo cambia el aspecto, no el significado.


3. Alertas y badges

3.1 Alertas

<div class="alert alert-success" role="alert">
  <h4 class="alert-heading">¡Pedido confirmado!</h4>
  <p class="mb-0">Recibirás un correo con los detalles. <a href="#" class="alert-link">Ver pedido</a>.</p>
</div>

<!-- Alerta que el usuario puede cerrar (necesita JS) -->
<div class="alert alert-warning alert-dismissible fade show" role="alert">
  <strong>Atención:</strong> tu sesión caducará en 5 minutos.
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar"></button>
</div>

Variantes: alert-primary, -secondary, -success, -danger, -warning, -info, -light, -dark.

3.2 Badges

<h2>Novedades <span class="badge text-bg-secondary">Nuevo</span></h2>
<span class="badge rounded-pill text-bg-success">Disponible</span>

<!-- Contador de notificaciones sobre un botón -->
<button type="button" class="btn btn-primary position-relative">
  Mensajes
  <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
    12 <span class="visually-hidden">mensajes sin leer</span>
  </span>
</button>

4. Cards

📘 Concepto: Una card (tarjeta) es un contenedor flexible con borde para agrupar contenido relacionado: imagen, título, texto, botones, cabecera y pie.

<div class="card" style="max-width: 20rem;">
  <img src="img/paella.jpg" class="card-img-top" alt="Paella valenciana en una paellera">
  <div class="card-body">
    <h5 class="card-title">Paella valenciana</h5>
    <p class="card-text">Receta tradicional con pollo, conejo, judía verde y garrofó.</p>
    <a href="#" class="btn btn-primary">Ver receta</a>
  </div>
  <div class="card-footer text-body-secondary">Tiempo: 60 min</div>
</div>
Clase Elemento
card Contenedor
card-header / card-footer Cabecera y pie
card-body Cuerpo con relleno
card-title, card-subtitle, card-text Textos
card-img-top, card-img-bottom Imagen arriba o abajo
card-img-overlay Texto encima de la imagen
stretched-link Hace clicable toda la tarjeta

4.1 Cards en rejilla con igual altura

💻 Ejemplo: Listado responsive de tarjetas. h-100 iguala las alturas y mt-auto empuja el botón al fondo.

<div class="container py-4">
  <div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-4">
    <div class="col">
      <article class="card h-100 shadow-sm">
        <img src="img/curso-html.jpg" class="card-img-top" alt="Portátil con código HTML">
        <div class="card-body d-flex flex-column">
          <span class="badge text-bg-primary align-self-start mb-2">Principiante</span>
          <h3 class="card-title h5">HTML desde cero</h3>
          <p class="card-text">Aprende la estructura de cualquier página web.</p>
          <a href="#" class="btn btn-outline-primary mt-auto stretched-link">Más información</a>
        </div>
      </article>
    </div>
    <div class="col">
      <article class="card h-100 shadow-sm">
        <img src="img/curso-css.jpg" class="card-img-top" alt="Paleta de colores">
        <div class="card-body d-flex flex-column">
          <span class="badge text-bg-success align-self-start mb-2">Intermedio</span>
          <h3 class="card-title h5">CSS moderno</h3>
          <p class="card-text">Flexbox, Grid, variables, :has() y mucho más. Este texto es más largo a propósito para comprobar que las tarjetas mantienen la misma altura.</p>
          <a href="#" class="btn btn-outline-primary mt-auto stretched-link">Más información</a>
        </div>
      </article>
    </div>
    <div class="col">
      <article class="card h-100 shadow-sm">
        <img src="img/curso-bootstrap.jpg" class="card-img-top" alt="Interfaz con componentes">
        <div class="card-body d-flex flex-column">
          <span class="badge text-bg-warning align-self-start mb-2">Avanzado</span>
          <h3 class="card-title h5">Bootstrap 5</h3>
          <p class="card-text">Maqueta interfaces profesionales en tiempo récord.</p>
          <a href="#" class="btn btn-outline-primary mt-auto stretched-link">Más información</a>
        </div>
      </article>
    </div>
  </div>
</div>

4.2 Card horizontal

<div class="card mb-3" style="max-width: 540px;">
  <div class="row g-0">
    <div class="col-md-4">
      <img src="img/autora.jpg" class="img-fluid rounded-start h-100 object-fit-cover" alt="Retrato de la autora">
    </div>
    <div class="col-md-8">
      <div class="card-body">
        <h5 class="card-title">Lucía Martín</h5>
        <p class="card-text">Diseñadora UX con 10 años de experiencia.</p>
        <p class="card-text"><small class="text-body-secondary">Madrid</small></p>
      </div>
    </div>
  </div>
</div>

5. Navbar responsive

La navbar es la barra de navegación principal. Con navbar-expand-{bp} decides a partir de qué breakpoint se muestra desplegada; por debajo, se convierte en un menú hamburguesa.

💻 Ejemplo: Navbar completa con marca, enlaces, desplegable y buscador.

<nav class="navbar navbar-expand-lg bg-body-tertiary sticky-top">
  <div class="container">
    <!-- Marca / logo -->
    <a class="navbar-brand fw-bold" href="index.html">☁️ Estudio Nube</a>

    <!-- Botón hamburguesa: solo visible por debajo de lg -->
    <button class="navbar-toggler" type="button"
            data-bs-toggle="collapse" data-bs-target="#menuPrincipal"
            aria-controls="menuPrincipal" aria-expanded="false" aria-label="Mostrar u ocultar el menú">
      <span class="navbar-toggler-icon"></span>
    </button>

    <!-- Contenido que se pliega -->
    <div class="collapse navbar-collapse" id="menuPrincipal">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="index.html">Inicio</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="portfolio.html">Portfolio</a>
        </li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" role="button"
             data-bs-toggle="dropdown" aria-expanded="false">Servicios</a>
          <ul class="dropdown-menu">
            <li><a class="dropdown-item" href="#">Diseño web</a></li>
            <li><a class="dropdown-item" href="#">Tiendas online</a></li>
            <li><hr class="dropdown-divider"></li>
            <li><a class="dropdown-item" href="#">Mantenimiento</a></li>
          </ul>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled" aria-disabled="true">Próximamente</a>
        </li>
      </ul>

      <form class="d-flex" role="search">
        <input class="form-control me-2" type="search" placeholder="Buscar" aria-label="Buscar">
        <button class="btn btn-outline-success" type="submit">Buscar</button>
      </form>
    </div>
  </div>
</nav>
Clase / atributo Función
navbar-expand-lg Desplegada desde lg; hamburguesa por debajo
navbar-brand Logo o nombre del sitio
navbar-toggler Botón hamburguesa
collapse navbar-collapse + id Zona plegable (el id debe coincidir con data-bs-target)
navbar-nav / nav-item / nav-link Lista de enlaces
me-auto Empuja lo siguiente (el buscador) a la derecha
active + aria-current="page" Página actual
data-bs-theme="dark" + bg-dark Navbar oscura (Bootstrap 5.3)

⚠️ Importante: En Bootstrap 5.3, la clase navbar-dark está desaconsejada. Usa <nav class="navbar bg-dark" data-bs-theme="dark"> para que el texto y el icono de la hamburguesa se vean claros.


6. Nav, tabs, breadcrumb y paginación

6.1 Nav y pestañas

<!-- Navegación simple en pestañas visuales (enlaces a otras páginas) -->
<ul class="nav nav-tabs">
  <li class="nav-item"><a class="nav-link active" aria-current="page" href="#">Perfil</a></li>
  <li class="nav-item"><a class="nav-link" href="#">Ajustes</a></li>
</ul>

<!-- Variante "pastillas" a todo el ancho -->
<ul class="nav nav-pills nav-fill my-3">
  <li class="nav-item"><a class="nav-link active" href="#">Todos</a></li>
  <li class="nav-item"><a class="nav-link" href="#">Web</a></li>
  <li class="nav-item"><a class="nav-link" href="#">Branding</a></li>
</ul>

Para que las pestañas cambien el contenido sin recargar la página, se usan data-bs-toggle="tab" y paneles tab-pane:

<ul class="nav nav-tabs" id="pestanasProducto" role="tablist">
  <li class="nav-item" role="presentation">
    <button class="nav-link active" id="desc-tab" data-bs-toggle="tab" data-bs-target="#desc"
            type="button" role="tab" aria-controls="desc" aria-selected="true">Descripción</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="espec-tab" data-bs-toggle="tab" data-bs-target="#espec"
            type="button" role="tab" aria-controls="espec" aria-selected="false">Especificaciones</button>
  </li>
</ul>
<div class="tab-content border border-top-0 p-3">
  <div class="tab-pane fade show active" id="desc" role="tabpanel" aria-labelledby="desc-tab" tabindex="0">
    Mochila impermeable de 20 litros.
  </div>
  <div class="tab-pane fade" id="espec" role="tabpanel" aria-labelledby="espec-tab" tabindex="0">
    Peso: 450 g. Material: poliéster reciclado.
  </div>
</div>

6.2 Breadcrumb (migas de pan)

<nav aria-label="Ruta de navegación">
  <ol class="breadcrumb">
    <li class="breadcrumb-item"><a href="index.html">Inicio</a></li>
    <li class="breadcrumb-item"><a href="tienda.html">Tienda</a></li>
    <li class="breadcrumb-item active" aria-current="page">Mochilas</li>
  </ol>
</nav>

<!-- Cambiar el separador con una variable CSS -->
<nav aria-label="Ruta" style="--bs-breadcrumb-divider: '›';">
  <ol class="breadcrumb">...</ol>
</nav>

6.3 Paginación

<nav aria-label="Paginación de resultados">
  <ul class="pagination justify-content-center">
    <li class="page-item disabled"><a class="page-link">Anterior</a></li>
    <li class="page-item active" aria-current="page"><a class="page-link" href="#">1</a></li>
    <li class="page-item"><a class="page-link" href="?pagina=2">2</a></li>
    <li class="page-item"><a class="page-link" href="?pagina=3">3</a></li>
    <li class="page-item"><a class="page-link" href="?pagina=2">Siguiente</a></li>
  </ul>
</nav>

Tamaños: pagination-lg y pagination-sm en el <ul>.


7. List group

<!-- Lista de enlaces con badge -->
<div class="list-group">
  <a href="#" class="list-group-item list-group-item-action active" aria-current="true">Bandeja de entrada</a>
  <a href="#" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
    Enviados <span class="badge text-bg-primary rounded-pill">14</span>
  </a>
  <a href="#" class="list-group-item list-group-item-action list-group-item-danger">Papelera</a>
</div>

<!-- Lista numerada sin bordes laterales (ideal dentro de cards) -->
<ol class="list-group list-group-numbered list-group-flush mt-3">
  <li class="list-group-item">Precalienta el horno</li>
  <li class="list-group-item">Mezcla los ingredientes</li>
  <li class="list-group-item">Hornea 25 minutos</li>
</ol>

8. Dropdowns

<div class="dropdown">
  <button class="btn btn-secondary dropdown-toggle" type="button"
          data-bs-toggle="dropdown" aria-expanded="false">
    Mi cuenta
  </button>
  <ul class="dropdown-menu dropdown-menu-end">
    <li><h6 class="dropdown-header">Ana García</h6></li>
    <li><a class="dropdown-item" href="#">Perfil</a></li>
    <li><a class="dropdown-item active" href="#" aria-current="true">Pedidos</a></li>
    <li><hr class="dropdown-divider"></li>
    <li><button class="dropdown-item text-danger" type="button">Cerrar sesión</button></li>
  </ul>
</div>
Clase Efecto
dropdown / dropup / dropend / dropstart Dirección en la que se abre
dropdown-menu-end Alinea el menú a la derecha del botón
dropdown-header, dropdown-divider Encabezado y separador
dropdown-menu-dark Menú oscuro (mejor: data-bs-theme="dark")

💡 Consejo: Los dropdowns usan Popper para posicionarse sin salirse de la pantalla. Por eso necesitas bootstrap.bundle.min.js y no bootstrap.min.js a secas.


9. Modal

📘 Concepto: Un modal es una ventana de diálogo que se superpone a la página y bloquea la interacción con el fondo hasta que se cierra. Úsalo para confirmaciones, formularios breves o detalles ampliados.

<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modalBorrar">
  Eliminar cuenta
</button>

<div class="modal fade" id="modalBorrar" tabindex="-1" aria-labelledby="modalBorrarTitulo" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="modalBorrarTitulo">¿Eliminar tu cuenta?</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
      </div>
      <div class="modal-body">
        Esta acción no se puede deshacer. Se borrarán todos tus datos.
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button type="button" class="btn btn-danger">Sí, eliminar</button>
      </div>
    </div>
  </div>
</div>
Opción Cómo
Tamaño modal-sm, modal-lg, modal-xl en .modal-dialog
Pantalla completa modal-fullscreen o modal-fullscreen-md-down
Centrado vertical modal-dialog-centered
Contenido largo con scroll interno modal-dialog-scrollable
No cerrar al pulsar fuera ni con Esc data-bs-backdrop="static" data-bs-keyboard="false" en .modal

También puedes controlarlo con JavaScript:

const modal = new bootstrap.Modal(document.getElementById('modalBorrar'));
modal.show();   // abrir
// modal.hide(); // cerrar

// Reaccionar a eventos: cuando el modal termina de abrirse
document.getElementById('modalBorrar').addEventListener('shown.bs.modal', () => {
  console.log('Modal visible');
});

⚠️ Importante: Coloca el HTML de los modales al final del <body> (fuera de contenedores con position o overflow especiales) para evitar problemas de superposición. Y no abras un modal encima de otro.


10. Offcanvas

Un offcanvas es un panel lateral que se desliza desde un borde de la pantalla: ideal para menús en móvil, filtros o un carrito.

<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas"
        data-bs-target="#panelFiltros" aria-controls="panelFiltros">
  Filtros
</button>

<div class="offcanvas offcanvas-end" tabindex="-1" id="panelFiltros" aria-labelledby="panelFiltrosTitulo">
  <div class="offcanvas-header">
    <h5 class="offcanvas-title" id="panelFiltrosTitulo">Filtrar productos</h5>
    <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Cerrar"></button>
  </div>
  <div class="offcanvas-body">
    <div class="form-check">
      <input class="form-check-input" type="checkbox" id="filtroOferta">
      <label class="form-check-label" for="filtroOferta">Solo ofertas</label>
    </div>
  </div>
</div>

Posiciones: offcanvas-start (izquierda), offcanvas-end (derecha), offcanvas-top, offcanvas-bottom. Con offcanvas-lg, el contenido se muestra normal desde lg y como panel por debajo.


<div id="carruselPortada" class="carousel slide carousel-fade" data-bs-ride="carousel">
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#carruselPortada" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Diapositiva 1"></button>
    <button type="button" data-bs-target="#carruselPortada" data-bs-slide-to="1" aria-label="Diapositiva 2"></button>
  </div>

  <div class="carousel-inner rounded-3">
    <div class="carousel-item active" data-bs-interval="6000">
      <img src="img/slide-1.jpg" class="d-block w-100" alt="Equipo trabajando en una oficina luminosa">
      <div class="carousel-caption d-none d-md-block">
        <h5>Diseño a medida</h5>
        <p>Cada proyecto es único.</p>
      </div>
    </div>
    <div class="carousel-item" data-bs-interval="6000">
      <img src="img/slide-2.jpg" class="d-block w-100" alt="Web vista en un móvil y un portátil">
      <div class="carousel-caption d-none d-md-block">
        <h5>Siempre responsive</h5>
        <p>Perfecto en cualquier pantalla.</p>
      </div>
    </div>
  </div>

  <button class="carousel-control-prev" type="button" data-bs-target="#carruselPortada" 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="#carruselPortada" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Siguiente</span>
  </button>
</div>
Atributo / clase Efecto
data-bs-ride="carousel" Empieza a rotar automáticamente al cargar
data-bs-interval="6000" Milisegundos de cada diapositiva
carousel-fade Fundido en lugar de deslizamiento
carousel-caption Texto superpuesto

⚠️ Importante: Los carruseles automáticos son poco accesibles y la mayoría de usuarios no pasa de la primera diapositiva. Si lo usas, pon el mensaje más importante en la primera, usa intervalos largos y no dependas de él para información crítica. Bootstrap pausa el carrusel al pasar el ratón y respeta prefers-reduced-motion en las transiciones.


12. Accordion y collapse

12.1 Collapse

<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse"
        data-bs-target="#masInfo" aria-expanded="false" aria-controls="masInfo">
  Mostrar más información
</button>
<div class="collapse mt-2" id="masInfo">
  <div class="card card-body">Contenido que aparece y desaparece con una animación.</div>
</div>

12.2 Accordion (preguntas frecuentes)

<div class="accordion" id="faq">
  <div class="accordion-item">
    <h2 class="accordion-header">
      <button class="accordion-button" type="button" data-bs-toggle="collapse"
              data-bs-target="#faq1" aria-expanded="true" aria-controls="faq1">
        ¿Cuánto tarda el envío?
      </button>
    </h2>
    <div id="faq1" class="accordion-collapse collapse show" data-bs-parent="#faq">
      <div class="accordion-body">Entre 24 y 48 horas en la península.</div>
    </div>
  </div>
  <div class="accordion-item">
    <h2 class="accordion-header">
      <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
              data-bs-target="#faq2" aria-expanded="false" aria-controls="faq2">
        ¿Puedo devolver un producto?
      </button>
    </h2>
    <div id="faq2" class="accordion-collapse collapse" data-bs-parent="#faq">
      <div class="accordion-body">Sí, dispones de 30 días naturales.</div>
    </div>
  </div>
</div>
Detalle Efecto
show en el panel + botón sin collapsed Panel abierto al cargar
data-bs-parent="#faq" Al abrir un panel se cierran los demás (quítalo para permitir varios abiertos)
accordion-flush Sin bordes ni esquinas, para insertarlo en otro contenedor

13. Toasts

Los toasts son notificaciones breves que aparecen en una esquina (“Guardado correctamente”). No se muestran solos: hay que lanzarlos con JavaScript.

<button type="button" class="btn btn-success" id="btnGuardar">Guardar</button>

<div class="toast-container position-fixed bottom-0 end-0 p-3">
  <div id="toastGuardado" class="toast" role="status" aria-live="polite" aria-atomic="true">
    <div class="toast-header">
      <strong class="me-auto">Estudio Nube</strong>
      <small>ahora</small>
      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Cerrar"></button>
    </div>
    <div class="toast-body">✅ Cambios guardados correctamente.</div>
  </div>
</div>

<script>
  const toast = bootstrap.Toast.getOrCreateInstance(document.getElementById('toastGuardado'));
  document.getElementById('btnGuardar').addEventListener('click', () => toast.show());
</script>

💡 Consejo: Los toasts se ocultan solos a los 5 segundos. Cámbialo con data-bs-delay="8000" o desactívalo con data-bs-autohide="false". Para mensajes importantes usa role="alert" aria-live="assertive".


14. Tooltips y popovers

⚠️ Importante: Por motivos de rendimiento, Bootstrap no activa automáticamente tooltips ni popovers. Si añades los atributos pero no el código de inicialización, no aparecerá nada.

<button type="button" class="btn btn-secondary"
        data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Se copiará al portapapeles">
  Copiar enlace
</button>

<button type="button" class="btn btn-info"
        data-bs-toggle="popover" data-bs-placement="right"
        data-bs-title="Envío gratuito"
        data-bs-content="En pedidos superiores a 50 € a la península.">
  ¿Envío gratis?
</button>

<!-- Popover que se cierra al hacer clic en cualquier otro sitio -->
<a tabindex="0" class="btn btn-link" role="button" data-bs-toggle="popover"
   data-bs-trigger="focus" data-bs-content="Haz clic fuera para cerrarme">Info</a>

<script>
  // Inicializar TODOS los tooltips de la página
  document.querySelectorAll('[data-bs-toggle="tooltip"]')
    .forEach(el => new bootstrap.Tooltip(el));

  // Inicializar TODOS los popovers de la página
  document.querySelectorAll('[data-bs-toggle="popover"]')
    .forEach(el => new bootstrap.Popover(el));
</script>
Opción Valores
data-bs-placement top, bottom, start… (left/right también)
data-bs-trigger hover focus (tooltip por defecto), click (popover por defecto), focus
data-bs-html="true" Permite HTML en el contenido (con cuidado)

💡 Consejo: No pongas información imprescindible solo en un tooltip: en móviles no existe el “hover”. Y nunca en elementos deshabilitados (disabled no recibe eventos); envuélvelos en un <span tabindex="0"> si lo necesitas.


15. Spinners, placeholders y progress

15.1 Spinners

<div class="spinner-border text-primary" role="status">
  <span class="visually-hidden">Cargando...</span>
</div>
<div class="spinner-grow text-success" role="status">
  <span class="visually-hidden">Cargando...</span>
</div>

<!-- Botón en estado de carga -->
<button class="btn btn-primary" type="button" disabled>
  <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
  <span role="status">Enviando...</span>
</button>

15.2 Placeholders (esqueletos de carga)

<div class="card" aria-hidden="true" style="max-width: 18rem;">
  <div class="card-body">
    <h5 class="card-title placeholder-glow">
      <span class="placeholder col-6"></span>
    </h5>
    <p class="card-text placeholder-glow">
      <span class="placeholder col-7"></span>
      <span class="placeholder col-4"></span>
      <span class="placeholder col-6"></span>
    </p>
    <a class="btn btn-primary disabled placeholder col-6" aria-disabled="true"></a>
  </div>
</div>

placeholder-glow hace un pulso de brillo; placeholder-wave, una onda.

15.3 Barras de progreso

<!-- Estructura de Bootstrap 5.3: los atributos ARIA van en .progress -->
<div class="progress" role="progressbar" aria-label="Progreso del curso"
     aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">
  <div class="progress-bar" style="width: 65%">65%</div>
</div>

<!-- Rayada, animada y de color -->
<div class="progress mt-3" role="progressbar" aria-label="Subiendo archivo"
     aria-valuenow="40" aria-valuemin="0" aria-valuemax="100">
  <div class="progress-bar progress-bar-striped progress-bar-animated bg-success" style="width: 40%"></div>
</div>

16. Formularios con Bootstrap

16.1 Controles básicos

<div class="mb-3">
  <label for="email" class="form-label">Correo electrónico</label>
  <input type="email" class="form-control" id="email" aria-describedby="emailAyuda">
  <div id="emailAyuda" class="form-text">Nunca compartiremos tu correo.</div>
</div>

<div class="mb-3">
  <label for="provincia" class="form-label">Provincia</label>
  <select class="form-select" id="provincia">
    <option selected disabled value="">Elige...</option>
    <option>Madrid</option>
    <option>Valencia</option>
  </select>
</div>

<div class="form-check">
  <input class="form-check-input" type="checkbox" id="boletin">
  <label class="form-check-label" for="boletin">Suscribirme al boletín</label>
</div>

<div class="form-check form-switch">
  <input class="form-check-input" type="checkbox" role="switch" id="notificaciones" checked>
  <label class="form-check-label" for="notificaciones">Notificaciones</label>
</div>

Los radios se escriben igual que los checkbox (type="radio" con el mismo name); añade form-check-inline al contenedor para ponerlos en línea.

Clase Elemento
form-label, form-text Etiqueta y texto de ayuda
form-control (-lg, -sm) input, textarea
form-select select
form-check, form-check-input, form-check-label Checkbox y radio
form-check-inline Checks o radios en la misma línea
form-switch Checkbox con aspecto de interruptor
form-range Control deslizante (input type="range")

16.2 Etiquetas flotantes e input groups

<!-- Etiqueta flotante: el input DEBE tener placeholder y la label va DESPUÉS -->
<div class="form-floating mb-3">
  <input type="email" class="form-control" id="emailFlotante" placeholder="nombre@ejemplo.com">
  <label for="emailFlotante">Correo electrónico</label>
</div>

<!-- Input group: texto o botones pegados al campo -->
<div class="input-group mb-3">
  <span class="input-group-text">@</span>
  <input type="text" class="form-control" placeholder="usuario" aria-label="Nombre de usuario">
</div>

<div class="input-group mb-3">
  <input type="number" class="form-control" aria-label="Importe en euros">
  <span class="input-group-text">€</span>
  <button class="btn btn-outline-secondary" type="button">Aplicar</button>
</div>

16.3 Layout y validación

💻 Ejemplo: Formulario de contacto completo en rejilla, con validación de Bootstrap.

<form class="row g-3 needs-validation" novalidate>
  <div class="col-md-6">
    <label for="nombre" class="form-label">Nombre</label>
    <input type="text" class="form-control" id="nombre" required>
    <div class="valid-feedback">¡Perfecto!</div>
    <div class="invalid-feedback">Escribe tu nombre.</div>
  </div>

  <div class="col-md-6">
    <label for="correo" class="form-label">Correo</label>
    <div class="input-group has-validation">
      <span class="input-group-text">@</span>
      <input type="email" class="form-control" id="correo" required>
      <div class="invalid-feedback">Introduce un correo válido.</div>
    </div>
  </div>

  <div class="col-md-4">
    <label for="asunto" class="form-label">Asunto</label>
    <select class="form-select" id="asunto" required>
      <option selected disabled value="">Elige...</option>
      <option>Presupuesto</option>
      <option>Soporte</option>
    </select>
    <div class="invalid-feedback">Selecciona un asunto.</div>
  </div>

  <div class="col-md-8">
    <label for="telefono" class="form-label">Teléfono (opcional)</label>
    <input type="tel" class="form-control" id="telefono" pattern="[0-9]{9}">
    <div class="invalid-feedback">Debe tener 9 dígitos.</div>
  </div>

  <div class="col-12">
    <label for="mensaje" class="form-label">Mensaje</label>
    <textarea class="form-control" id="mensaje" rows="4" minlength="10" required></textarea>
    <div class="invalid-feedback">El mensaje debe tener al menos 10 caracteres.</div>
  </div>

  <div class="col-12">
    <div class="form-check">
      <input class="form-check-input" type="checkbox" id="privacidad" required>
      <label class="form-check-label" for="privacidad">Acepto la política de privacidad</label>
      <div class="invalid-feedback">Debes aceptarla para continuar.</div>
    </div>
  </div>

  <div class="col-12">
    <button class="btn btn-primary" type="submit">Enviar</button>
  </div>
</form>

<script>
  // Validación de Bootstrap: usa la validación nativa de HTML5 y muestra los mensajes
  document.querySelectorAll('.needs-validation').forEach(form => {
    form.addEventListener('submit', event => {
      if (!form.checkValidity()) {
        event.preventDefault();   // no enviar si hay errores
        event.stopPropagation();
      }
      form.classList.add('was-validated'); // activa los estilos verde/rojo
    });
  });
</script>
Pieza Función
novalidate Desactiva las burbujas nativas del navegador para mostrar las de Bootstrap
required, pattern, minlength, type="email" Reglas de validación HTML5 (las viste en el módulo 05)
.was-validated en el <form> Muestra los estilos :valid / :invalid
valid-feedback / invalid-feedback Mensajes que aparecen según el estado
has-validation Necesaria en input-group para redondear bien las esquinas
is-valid / is-invalid Marcar un campo manualmente (por ejemplo, tras validar en el servidor)

⚠️ Importante: La validación en el navegador mejora la experiencia, pero no es seguridad. Cualquier usuario puede saltársela. El servidor siempre debe volver a validar los datos.


17. Tablas

<div class="table-responsive">
  <table class="table table-striped table-hover align-middle">
    <caption>Pedidos del último mes</caption>
    <thead class="table-light">
      <tr>
        <th scope="col">#</th>
        <th scope="col">Cliente</th>
        <th scope="col">Estado</th>
        <th scope="col" class="text-end">Importe</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">1001</th>
        <td>Ana García</td>
        <td><span class="badge text-bg-success">Enviado</span></td>
        <td class="text-end">89,90 €</td>
      </tr>
      <tr class="table-warning">
        <th scope="row">1002</th>
        <td>Luis Pérez</td>
        <td><span class="badge text-bg-warning">Pendiente</span></td>
        <td class="text-end">45,00 €</td>
      </tr>
    </tbody>
  </table>
</div>
Clase Efecto
table Estilo base (obligatoria)
table-striped Filas alternas sombreadas
table-hover Resalta la fila bajo el ratón
table-bordered / table-borderless Con o sin bordes en todas las celdas
table-sm Celdas más compactas
table-dark, table-primary… Color en tabla, fila o celda
align-middle Centrado vertical del contenido
table-responsive (en un div contenedor) Scroll horizontal en pantallas estrechas

18. Errores comunes

Síntoma Causa Solución
La hamburguesa no abre el menú Falta el JS o el id no coincide con data-bs-target Revisa el <script> y que #id sea idéntico
Dropdowns mal posicionados o sin abrir Se cargó bootstrap.min.js sin Popper Usa bootstrap.bundle.min.js
Los tooltips no aparecen No se inicializaron Añade el código new bootstrap.Tooltip(el)
Atributos data-toggle no funcionan Sintaxis de Bootstrap 4 En Bootstrap 5 es data-bs-toggle
La etiqueta flotante se superpone al texto Falta placeholder o la label está antes del input placeholder obligatorio y label después
Mensajes invalid-feedback nunca se ven Falta .was-validated o el mensaje no es hermano del campo Añade la clase con JS y coloca el mensaje justo después
El modal aparece detrás del fondo oscuro El modal está dentro de un contenedor con position/z-index Mueve el modal al final del <body>

19. Ejercicios

Ejercicio 1: Panel de avisos

Crea una página con cuatro alertas (éxito, información, advertencia y error), cada una con un título alert-heading. Haz que la de advertencia se pueda cerrar. Añade un botón con un badge contador posicionado en la esquina.

Ejercicio 2: Catálogo con cards

Maqueta un catálogo de 6 productos con row-cols-*: imagen, badge de categoría, título, precio y botón “Añadir” alineado al fondo de cada tarjeta. Todas las tarjetas deben tener la misma altura aunque el texto varíe.

Ejercicio 3: Navbar profesional

Construye una navbar oscura (data-bs-theme="dark") que se expanda desde md, con logo, cuatro enlaces (uno activo con aria-current), un desplegable “Más” y un botón “Acceder” alineado a la derecha. Comprueba con el teclado (Tab, Enter, Esc) que todo es accesible.

Ejercicio 4: Ficha de producto

Crea una ficha con breadcrumb, un carrusel de 3 fotos con controles, pestañas (Descripción / Especificaciones / Opiniones) y un acordeón de preguntas frecuentes. Al pulsar “Comprar”, abre un modal de confirmación; al confirmar, ciérralo y muestra un toast “Añadido al carrito”.

Ejercicio 5: Ayudas contextuales

Añade a un formulario de registro tres tooltips (en iconos de ayuda junto a las etiquetas) y un popover que explique los requisitos de la contraseña. Recuerda inicializarlos con JavaScript.

Ejercicio 6: Formulario validado

Crea un formulario de reserva de mesa con: nombre, email, teléfono (9 dígitos), fecha, número de comensales (form-select), zona (radios inline: terraza/interior), alergias (textarea con form-floating) y aceptación de condiciones. Distribúyelo en rejilla de dos columnas desde md y valídalo con .was-validated.

Ejercicio 7: Panel de administración

Combina un offcanvas como menú lateral en móvil, una tabla responsive de usuarios con table-hover y badges de estado, paginación centrada, placeholders mientras “carga” (simula 2 segundos con setTimeout y luego sustituye el contenido) y una barra de progreso de “espacio utilizado”.


Resumen

Componente Clases / atributos clave ¿JS?
Botones btn btn-{color}, btn-outline-*, btn-lg, btn-group, btn-check No
Alertas alert alert-{color}, alert-dismissible, data-bs-dismiss="alert" Solo para cerrar
Badges badge text-bg-{color}, rounded-pill No
Cards card, card-body, card-title, card-img-top, h-100 No
Navbar navbar navbar-expand-lg, navbar-toggler, collapse navbar-collapse Sí
Nav / tabs nav nav-tabs, nav-pills, data-bs-toggle="tab", tab-pane Para tabs
Breadcrumb breadcrumb, breadcrumb-item, aria-current="page" No
Paginación pagination, page-item, page-link No
List group list-group, list-group-item, -action, -flush No
Dropdown dropdown, dropdown-toggle, dropdown-menu, dropdown-item Sí
Modal modal fade, modal-dialog, modal-content, data-bs-toggle="modal" Sí
Offcanvas offcanvas offcanvas-start, data-bs-toggle="offcanvas" Sí
Carousel carousel slide, carousel-item, data-bs-ride Sí
Accordion / collapse accordion-item, accordion-button, collapse, data-bs-parent Sí
Toasts toast, bootstrap.Toast.getOrCreateInstance(el).show() Sí, inicializar
Tooltips / popovers data-bs-toggle="tooltip", new bootstrap.Tooltip(el) Sí, inicializar
Spinners / placeholders spinner-border, placeholder, placeholder-glow No
Progress progress (con ARIA) + progress-bar No
Formularios form-control, form-select, form-check, form-floating, input-group No
Validación needs-validation, novalidate, .was-validated, invalid-feedback Sí
Tablas table, table-striped, table-hover, table-responsive No