Módulo 17: Bootstrap: personalización

Objetivos del módulo

  • Sobrescribir estilos de Bootstrap con CSS propio controlando la especificidad
  • Personalizar Bootstrap en tiempo real con sus variables CSS (--bs-*)
  • Implementar modo oscuro con data-bs-theme y un conmutador en JavaScript
  • Usar Bootstrap Icons como fuente y como SVG
  • Compilar un Bootstrap a medida con Sass: colores, variables, mapas e importación parcial
  • Crear utilidades propias con la API de utilidades
  • Aplicar buenas prácticas de accesibilidad con Bootstrap
  • Saber cuándo no usar Bootstrap y cómo se compara con Tailwind CSS

1. Tres niveles de personalización

Personalizar Bootstrap es como reformar un piso de alquiler amueblado: puedes cambiar cojines y cortinas (CSS propio), pintar las paredes (variables CSS) o hacer obra y tirar tabiques (Sass). Cada nivel requiere más esfuerzo, pero da más control.

Nivel Herramienta Requiere instalación Control Ideal para
1 CSS propio que sobrescribe No (funciona con CDN) Bajo-medio Ajustes puntuales
2 Variables CSS --bs-* No (funciona con CDN) Medio Colores, fuentes, temas, modo oscuro
3 Sass Sí (Node.js + npm) Total Marca propia, reducir tamaño, utilidades nuevas
flowchart LR
    A["¿Qué quieres cambiar?"] --> B{"¿Un detalle concreto?"}
    B -- Sí --> C["CSS propio"]
    B -- No --> D{"¿Colores, fuentes, modo oscuro en tiempo real?"}
    D -- Sí --> E["Variables CSS --bs-*"]
    D -- No --> F["Sass: compilar Bootstrap a medida"]

2. Sobrescribir estilos con CSS propio

2.1 Orden de carga

La regla básica de la cascada (módulo 07): con la misma especificidad, gana la última regla. Por eso tu CSS va después de Bootstrap:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="css/estilos.css" rel="stylesheet"> <!-- después -->
/* estilos.css: misma especificidad que en Bootstrap (una clase) → gana la nuestra */
.card {
  border-radius: 1rem;
  border: none;
  box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 0.08);
}

2.2 Cuando no basta: especificidad

Algunos selectores de Bootstrap son más específicos. Usa DevTools (clic derecho → Inspeccionar → pestaña Styles) para ver qué regla se aplica y con qué selector.

/* Bootstrap usa ".navbar-nav .nav-link.active" (3 clases).
   Una sola clase .nav-link no basta; igualamos la especificidad: */
.navbar-nav .nav-link.active {
  font-weight: 700;
  border-bottom: 2px solid currentColor;
}

💡 Consejo: Crea clases propias en lugar de modificar las de Bootstrap para todo el sitio: .card-producto en vez de cambiar .card. Así no rompes otras tarjetas y el código explica su intención.

<div class="card card-producto">...</div>
.card-producto { transition: transform 0.2s; }
.card-producto:hover { transform: translateY(-4px); }

2.3 Capas de cascada con Bootstrap

Como viste en el módulo 14, puedes meter Bootstrap en una capa para que cualquier regla tuya gane sin preocuparte por la especificidad:

/* estilos.css (enlaza SOLO este fichero en el HTML) */
@layer bootstrap, tema;

@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css") layer(bootstrap);

@layer tema {
  .nav-link { font-weight: 600; } /* gana a .navbar-nav .nav-link.active */
}

⚠️ Importante: Bootstrap usa !important en sus utilidades (.d-none, .mt-3…) para que siempre ganen. Recuerda que con !important el orden de capas se invierte: dentro de una capa anterior, el !important de Bootstrap seguirá ganando a tus !important. Es el comportamiento deseado: las utilidades deben ganar.

⚠️ Importante: Evita llenar tu CSS de !important para “ganar” a Bootstrap. Es una señal de que debes usar una clase propia, igualar la especificidad, usar capas o, mejor aún, variables CSS o Sass.


3. Variables CSS de Bootstrap

3.1 Variables globales

Bootstrap 5.3 expone cientos de custom properties con el prefijo --bs- en :root. Puedes verlas en DevTools seleccionando el elemento <html>.

Variable Qué controla
--bs-body-font-family Fuente de todo el documento
--bs-body-font-size Tamaño base del texto
--bs-body-color / --bs-body-bg Color de texto y de fondo de la página
--bs-primary, --bs-secondary… Colores del tema (en hexadecimal)
--bs-primary-rgb El mismo color en canales RGB (13, 110, 253) para usar con opacidad
--bs-link-color / --bs-link-hover-color Enlaces
--bs-border-radius Radio de esquinas por defecto
--bs-border-color Color de bordes
--bs-secondary-color Texto atenuado (text-body-secondary)
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap");

:root {
  --bs-body-font-family: "Inter", system-ui, sans-serif;
  --bs-body-color: #1f2933;
  --bs-border-radius: 0.75rem;

  /* Para cambiar un color de tema, cambia la versión hex Y la rgb */
  --bs-primary: #6f42c1;
  --bs-primary-rgb: 111, 66, 193;
  --bs-link-color: #6f42c1;
  --bs-link-color-rgb: 111, 66, 193;
}

💡 Consejo: Para cargar una fuente de Google Fonts es preferible enlazarla con <link> en el <head> (con preconnect) en lugar de @import, porque @import retrasa la descarga. Aquí usamos @import solo para que el ejemplo quepa en un fichero.

3.2 Variables de componente

⚠️ Importante: Cambiar --bs-primary no cambia el color de .btn-primary. Los botones y muchos componentes tienen sus propias variables locales (calculadas al compilar con Sass), como --bs-btn-bg. Para modificarlos, cambia esas variables en el componente.

/* Botón primario morado, con sus estados */
.btn-primary {
  --bs-btn-bg: #6f42c1;
  --bs-btn-border-color: #6f42c1;
  --bs-btn-hover-bg: #59339d;
  --bs-btn-hover-border-color: #59339d;
  --bs-btn-active-bg: #4c2a86;
  --bs-btn-active-border-color: #4c2a86;
  --bs-btn-disabled-bg: #6f42c1;
  --bs-btn-disabled-border-color: #6f42c1;
  --bs-btn-focus-shadow-rgb: 111, 66, 193;
}

/* Navbar: color de los enlaces */
.navbar {
  --bs-navbar-color: rgb(255 255 255 / 0.8);
  --bs-navbar-hover-color: #fff;
  --bs-navbar-active-color: #fff;
}

/* Cards: relleno y radio */
.card {
  --bs-card-spacer-y: 1.5rem;
  --bs-card-spacer-x: 1.5rem;
  --bs-card-border-radius: 1rem;
}

💡 Consejo: Cada página de componente de la documentación tiene una sección “CSS → Variables” con la lista completa de sus variables. Es la forma más limpia de personalizar un componente sin Sass.

3.3 Variables a nivel local

Como son variables CSS, puedes cambiarlas solo en una zona o elemento, incluso desde el atributo style:

<!-- Solo esta sección tiene enlaces naranjas -->
<section style="--bs-link-color-rgb: 253, 126, 20;">
  <a href="#">Enlace naranja</a>
</section>

<!-- Un botón concreto con otro radio -->
<button class="btn btn-dark" style="--bs-btn-border-radius: 2rem;">Píldora</button>

4. Modo oscuro con data-bs-theme

4.1 Activar el modo oscuro

Desde la versión 5.3, Bootstrap incluye modos de color. Basta un atributo:

<!-- Toda la página en modo oscuro -->
<html lang="es" data-bs-theme="dark">
<!-- O solo un fragmento: una tarjeta oscura dentro de una página clara -->
<div class="card" data-bs-theme="dark">
  <div class="card-body">Tarjeta oscura</div>
</div>

📘 Concepto: data-bs-theme="dark" redefine variables como --bs-body-bg, --bs-body-color o --bs-border-color. Los componentes y utilidades que usan esas variables (bg-body, bg-body-tertiary, text-body-secondary, cards, modales, formularios…) se adaptan solos.

⚠️ Importante: Clases como bg-light, bg-white o text-dark son colores fijos y no cambian con el modo. Si quieres que se adapten, usa las versiones semánticas: bg-body, bg-body-tertiary, text-body, text-body-emphasis, border, *-subtle, *-emphasis.

4.2 Conmutador claro / oscuro / automático

💻 Ejemplo: Conmutador completo que recuerda la preferencia del usuario y respeta la del sistema operativo.

<!doctype html>
<html lang="es" data-bs-theme="light">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Modo oscuro</title>
  <!-- Aplicamos el tema ANTES de pintar la página para evitar un parpadeo -->
  <script>
    (() => {
      const guardado = localStorage.getItem('tema') || 'auto';
      const oscuroSistema = window.matchMedia('(prefers-color-scheme: dark)').matches;
      const tema = guardado === 'auto' ? (oscuroSistema ? 'dark' : 'light') : guardado;
      document.documentElement.setAttribute('data-bs-theme', tema);
    })();
  </script>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css" rel="stylesheet">
</head>
<body>
  <nav class="navbar bg-body-tertiary border-bottom">
    <div class="container">
      <span class="navbar-brand">Mi web</span>

      <div class="dropdown">
        <button class="btn btn-outline-secondary dropdown-toggle" type="button"
                data-bs-toggle="dropdown" aria-expanded="false" aria-label="Cambiar tema">
          <i class="bi bi-circle-half" aria-hidden="true"></i> Tema
        </button>
        <ul class="dropdown-menu dropdown-menu-end">
          <li><button class="dropdown-item" type="button" data-tema="light">
            <i class="bi bi-sun-fill me-2" aria-hidden="true"></i>Claro</button></li>
          <li><button class="dropdown-item" type="button" data-tema="dark">
            <i class="bi bi-moon-stars-fill me-2" aria-hidden="true"></i>Oscuro</button></li>
          <li><button class="dropdown-item" type="button" data-tema="auto">
            <i class="bi bi-circle-half me-2" aria-hidden="true"></i>Automático</button></li>
        </ul>
      </div>
    </div>
  </nav>

  <main class="container py-5">
    <h1>Hola</h1>
    <p class="text-body-secondary">Cambia el tema desde el menú superior.</p>
    <div class="card"><div class="card-body">Esta tarjeta se adapta sola.</div></div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    const consultaOscuro = window.matchMedia('(prefers-color-scheme: dark)');

    function aplicarTema(preferencia) {
      const tema = preferencia === 'auto'
        ? (consultaOscuro.matches ? 'dark' : 'light')
        : preferencia;
      document.documentElement.setAttribute('data-bs-theme', tema);

      // Marcar la opción activa en el menú (accesible)
      document.querySelectorAll('[data-tema]').forEach(boton => {
        const activo = boton.dataset.tema === preferencia;
        boton.classList.toggle('active', activo);
        boton.setAttribute('aria-pressed', activo);
      });
    }

    // Al pulsar una opción: guardar y aplicar
    document.querySelectorAll('[data-tema]').forEach(boton => {
      boton.addEventListener('click', () => {
        localStorage.setItem('tema', boton.dataset.tema);
        aplicarTema(boton.dataset.tema);
      });
    });

    // Si el usuario está en "auto" y cambia el tema del sistema, lo seguimos
    consultaOscuro.addEventListener('change', () => {
      if ((localStorage.getItem('tema') || 'auto') === 'auto') aplicarTema('auto');
    });

    aplicarTema(localStorage.getItem('tema') || 'auto');
  </script>
</body>
</html>
Pieza Para qué
Script en el <head> Fija el tema antes de pintar: evita el “destello” blanco al cargar en modo oscuro
localStorage Recuerda la elección entre visitas
matchMedia('(prefers-color-scheme: dark)') Lee la preferencia del sistema operativo
Evento change Reacciona si el sistema cambia de modo (por ejemplo, al anochecer)

4.3 Personalizar los colores del modo oscuro

/* Ajustar el modo oscuro de Bootstrap a tu marca */
[data-bs-theme="dark"] {
  --bs-body-bg: #0f172a;
  --bs-body-color: #e2e8f0;
  --bs-tertiary-bg: #1e293b;
  --bs-border-color: #334155;
}

/* Tus propios estilos también pueden depender del modo */
[data-bs-theme="dark"] .hero {
  background-image: linear-gradient(135deg, #1e1b4b, #0f172a);
}

5. Bootstrap Icons

Bootstrap Icons es una librería oficial y gratuita con más de 2.000 iconos SVG. Se puede usar con cualquier proyecto (no necesita Bootstrap).

5.1 Como fuente de iconos (CDN)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<!-- Icono decorativo junto a un texto: lo ocultamos a los lectores de pantalla -->
<button class="btn btn-primary">
  <i class="bi bi-download me-1" aria-hidden="true"></i> Descargar
</button>

<!-- Icono que transmite información por sí solo: necesita texto alternativo -->
<a href="https://github.com/" class="fs-3 link-body-emphasis" aria-label="GitHub">
  <i class="bi bi-github" aria-hidden="true"></i>
</a>

<!-- Tamaño y color con utilidades: los iconos son texto -->
<i class="bi bi-heart-fill text-danger fs-1" aria-hidden="true"></i>
<i class="bi bi-star-fill text-warning" aria-hidden="true"></i>

💡 Consejo: Busca iconos en icons.getbootstrap.com. Cada icono tiene su clase (bi-nombre); muchos tienen variante rellena (-fill).

5.2 Como SVG

La fuente carga todos los iconos aunque uses cinco. Con SVG solo incluyes los que necesitas, y además puedes animarlos o estilarlos con más detalle.

<!-- SVG copiado de la web de Bootstrap Icons (botón "Copy HTML") -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"
     class="bi bi-check-circle-fill text-success" viewBox="0 0 16 16" aria-hidden="true">
  <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>

Si instalas los iconos con npm (npm install bootstrap-icons), puedes usar un sprite: un único fichero SVG con todos los iconos, del que eliges uno con <use>:

<svg class="bi" width="32" height="32" fill="currentColor" aria-hidden="true">
  <use href="node_modules/bootstrap-icons/bootstrap-icons.svg#heart-fill"></use>
</svg>
Método Ventajas Inconvenientes
Fuente (CDN) Facilísimo; tamaño y color con clases de texto Descarga todos los iconos
SVG en línea Solo los que usas, sin peticiones extra HTML más largo
Sprite SVG Un fichero cacheable, HTML limpio Requiere servir el fichero sprite

💡 Consejo: fill="currentColor" hace que el SVG tome el color del texto, así que text-danger o link-primary funcionan igual que con la fuente.


6. Personalización con Sass

6.1 ¿Por qué Sass?

Bootstrap está escrito en Sass, un lenguaje que se compila a CSS. Compilarlo tú mismo te permite:

  • Cambiar los colores de marca en todos los componentes a la vez (botones, alertas, badges, enlaces…).
  • Modificar opciones globales: radios, sombras, degradados, espaciados, breakpoints.
  • Añadir colores nuevos que generen clases (btn-marca, text-bg-marca…).
  • Incluir solo las partes que uses y reducir el tamaño del CSS final.

6.2 Preparar el proyecto

mkdir web-a-medida
cd web-a-medida
npm init -y
npm install bootstrap@5.3.8
npm install --save-dev sass

Estructura recomendada:

web-a-medida/
├── node_modules/
├── scss/
│   └── estilos.scss     ← tu fichero Sass
├── css/
│   └── estilos.css      ← lo genera el compilador (no lo edites)
├── index.html
└── package.json

Añade estos scripts a package.json:

{
  "scripts": {
    "css": "sass --load-path=node_modules --quiet-deps --style=compressed scss/estilos.scss css/estilos.css",
    "css:watch": "sass --load-path=node_modules --quiet-deps --watch scss/estilos.scss css/estilos.css"
  }
}
Opción Significado
--load-path=node_modules Permite escribir @import "bootstrap/scss/..." sin rutas relativas
--quiet-deps Oculta los avisos que vienen de dependencias (Bootstrap)
--style=compressed CSS minificado para producción
--watch Recompila automáticamente al guardar
npm run css:watch   # mientras desarrollas
npm run css         # versión final minificada

⚠️ Importante: Bootstrap 5.3 usa la regla @import de Sass, que Dart Sass ha marcado como obsoleta. Al compilar verás avisos de deprecación (deprecation warnings). La documentación oficial indica que no impiden la compilación y que pueden ignorarse; --quiet-deps oculta los que vienen de Bootstrap. Son avisos, no errores.

6.3 Opción A: todo Bootstrap con tus variables

// scss/estilos.scss

// 1. Tus variables ANTES de importar Bootstrap
$primary:   #6f42c1;
$secondary: #6c757d;
$success:   #2a9d8f;
$danger:    #e63946;

$font-family-sans-serif: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
$border-radius:    0.75rem;
$border-radius-lg: 1rem;
$enable-shadows:   true;
$enable-gradients: false;
$link-decoration:  none;

// 2. Bootstrap completo
@import "bootstrap/scss/bootstrap";

// 3. Tus estilos propios, que ya pueden usar variables y mixins de Bootstrap
.hero {
  padding-block: $spacer * 4;
  background: $primary;
  color: $white;

  @include media-breakpoint-up(lg) {
    padding-block: $spacer * 6;
  }
}

📘 Concepto: Las variables de Bootstrap están definidas con !default, que significa “usa este valor solo si no se ha definido antes”. Por eso tus variables deben ir antes del @import: cuando Bootstrap llega, respeta tu valor.

Enlaza el CSS resultante en lugar del CDN:

<link rel="stylesheet" href="css/estilos.css">
<script src="node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>

Variables muy útiles:

Variable Valor por defecto Efecto
$primary, $success… Colores del tema Colores de todos los componentes
$font-family-sans-serif Pila de fuentes del sistema Fuente principal
$font-size-base 1rem Tamaño base
$spacer 1rem Base de la escala de espaciado (m-3 = $spacer)
$border-radius 0.375rem Radio por defecto
$enable-shadows false Sombras en botones, inputs…
$enable-gradients false Degradados sutiles en componentes
$enable-dark-mode true Generar el CSS del modo oscuro
$grid-breakpoints sm 576px ... xxl 1400px Breakpoints
$container-max-widths 540px ... 1320px Anchos de .container

6.4 Mapas: $theme-colors

Los colores del tema se guardan en un mapa de Sass (una lista de pares clave-valor). De ese mapa salen las clases btn-*, bg-*, text-*, alert-*, border-*… Si añades un color al mapa, Bootstrap genera todas sus clases.

Para modificar mapas necesitas la importación por partes (opción B), porque el mapa debe existir antes de modificarlo:

// scss/estilos.scss

// 1. Funciones (necesarias para manipular colores)
@import "bootstrap/scss/functions";

// 2. Tus variables
$primary: #6f42c1;

// 3. Variables de Bootstrap
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";

// 4. Modificar mapas: añadir colores y quitar los que no usas
$colores-propios: (
  "marca":  #ff6b35,
  "oceano": #0077b6
);
$theme-colors: map-merge($theme-colors, $colores-propios);
$theme-colors: map-remove($theme-colors, "info", "light");

// 5. Resto de Bootstrap (functions y variables se vuelven a leer, pero no generan
//    CSS y, gracias a !default, respetan los valores y mapas que ya has definido)
@import "bootstrap/scss/bootstrap";
<!-- Ahora existen estas clases -->
<button class="btn btn-marca">Botón marca</button>
<button class="btn btn-outline-oceano">Contorno océano</button>
<span class="badge text-bg-oceano">Océano</span>
<p class="text-marca">Texto con el color de marca</p>

⚠️ Importante: En Bootstrap 5.3, las alertas, las variantes de list group y las utilidades *-subtle / *-emphasis no usan $theme-colors directamente, sino mapas adicionales ($theme-colors-text, $theme-colors-bg-subtle, $theme-colors-border-subtle y sus equivalentes -dark). Si quieres alert-marca, tendrás que añadir tu color también a esos mapas; si no, la clase se genera pero sin colores.

6.5 Opción B: importación parcial para reducir tamaño

Si solo usas la rejilla, las utilidades, botones, navbar y cards, ¿para qué cargar carruseles, toasts o popovers?

// scss/estilos.scss — Bootstrap "a la carta"

// 1. Configuración (obligatoria, no genera CSS)
@import "bootstrap/scss/functions";

$primary: #6f42c1;   // tus variables aquí

@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";

// 2. Base
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/helpers";

// 3. Solo los componentes que uso
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
@import "bootstrap/scss/card";
@import "bootstrap/scss/forms";

// 4. API de utilidades AL FINAL (genera las clases m-*, d-*, text-*...)
@import "bootstrap/scss/utilities/api";

// 5. Tu código
Grupo Ficheros ¿Genera CSS?
Configuración functions, variables, variables-dark, maps, mixins, utilities No (solo definiciones)
Base root, reboot, type, images, containers, grid, helpers Sí
Componentes buttons, navbar, card, modal, carousel… Sí
Utilidades utilities/api Sí

💡 Consejo: Algunos componentes dependen de otros: la navbar usa nav y transitions (para el collapse), los dropdowns necesitan dropdown… Si algo se ve roto tras una importación parcial, revisa en node_modules/bootstrap/scss/bootstrap.scss el orden completo y qué te falta.

💡 Consejo: El JavaScript también se puede reducir: en vez del bundle completo, puedes importar solo los plugins que necesites con un empaquetador (por ejemplo Vite) usando import Modal from 'bootstrap/js/dist/modal';.


7. La API de utilidades

📘 Concepto: Todas las utilidades de Bootstrap (m-*, d-*, text-*, opacity-*…) se generan a partir de un mapa Sass llamado $utilities. Si añades una entrada a ese mapa, Bootstrap genera las clases correspondientes, incluidas sus versiones responsive.

7.1 Crear una utilidad nueva

@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";

// Añadimos utilidades ANTES de utilities/api
$utilities: map-merge(
  $utilities,
  (
    // Genera: .cursor-auto, .cursor-pointer, .cursor-grab y versiones .cursor-md-pointer...
    "cursor": (
      property: cursor,
      class: cursor,
      responsive: true,
      values: auto pointer grab not-allowed,
    ),
    // Genera: .ratio-img-1x1, .ratio-img-4x3, .ratio-img-16x9
    "aspect-ratio": (
      property: aspect-ratio,
      class: ratio-img,
      values: (
        "1x1": 1,
        "4x3": calc(4 / 3),
        "16x9": calc(16 / 9),
      ),
    ),
  )
);

// Resto de Bootstrap: incluye utilities/api, que genera también tus clases nuevas
@import "bootstrap/scss/bootstrap";
<div class="card cursor-pointer">Tarjeta clicable</div>
<img src="foto.jpg" class="w-100 ratio-img-4x3 object-fit-cover" alt="Foto recortada a 4:3">
Opción Para qué
property Propiedad CSS (o lista de propiedades)
class Prefijo de la clase generada
values Lista (a b c) o mapa ("nombre": valor)
responsive: true Genera también -sm-, -md-, -lg-…
print: true Genera la versión -print-
state: hover Genera variantes de estado como .opacity-50-hover

7.2 Eliminar utilidades que no usas

// Quitar las utilidades de ancho/alto si no las usas (reduce el CSS)
$utilities: map-merge(
  $utilities,
  (
    "width": null,
    "height": null,
  )
);

8. Accesibilidad en Bootstrap

Bootstrap ofrece una buena base, pero la accesibilidad final depende de ti: del HTML, del contenido y de tus personalizaciones.

Buena práctica Cómo con Bootstrap
Enlace “saltar al contenido” <a class="visually-hidden-focusable" href="#contenido">Saltar al contenido</a>
Texto solo para lectores de pantalla visually-hidden
Foco visible No elimines el outline; puedes usar la utilidad focus-ring en elementos propios
Contraste suficiente Comprueba tus colores personalizados (mínimo 4.5:1 en texto normal)
Movimiento reducido Bootstrap reduce las transiciones con prefers-reduced-motion; respétalo en tu CSS
Atributos ARIA de los componentes Cópialos de la documentación y actualízalos (aria-current, aria-expanded…)
Iconos aria-hidden="true" si son decorativos, aria-label si transmiten información
Formularios Siempre <label for>; mensajes con invalid-feedback y aria-describedby
<body>
  <a class="visually-hidden-focusable" href="#contenido">Saltar al contenido principal</a>
  <nav class="navbar">...</nav>
  <main id="contenido">...</main>
</body>

⚠️ Importante: Al cambiar $primary u otros colores, Bootstrap calcula el color de texto de botones y badges según $min-contrast-ratio (4.5 por defecto), pero no comprueba todas las combinaciones. Verifica tus colores con el panel de accesibilidad de DevTools o con Lighthouse.


9. Buenas prácticas y cuándo NO usar Bootstrap

9.1 Buenas prácticas

  • Fija la versión (bootstrap@5.3.8), nunca uses “latest” en producción.
  • Usa las utilidades para ajustes de maquetación y clases propias para estilos de marca.
  • No mezcles HTML de Bootstrap 4 y 5: revisa siempre la versión de los ejemplos que copies.
  • Personaliza: cambia al menos colores, tipografía y radios para que tu web no “huela a Bootstrap”.
  • No cargues el JS si no usas componentes interactivos; y en producción, valora compilar solo lo necesario.
  • Respeta la estructura de los componentes y sus atributos ARIA.
  • No abuses de las utilidades: si repites la misma combinación de 10 clases en 20 sitios, crea una clase o un componente.
<!-- ❌ Difícil de mantener si se repite en muchas páginas -->
<a class="btn btn-primary btn-lg rounded-pill px-5 py-3 shadow fw-bold text-uppercase">Comprar</a>

<!-- ✅ Una clase con significado -->
<a class="btn btn-primary btn-cta">Comprar</a>

9.2 Cuándo NO usar Bootstrap

Situación Por qué Alternativa
Diseño muy personalizado o de marca fuerte Pasarás más tiempo deshaciendo estilos que creándolos CSS propio con Grid, Flex y variables
Una landing muy pequeña y rápida El CSS de Bootstrap pesa más que toda tu página CSS a mano
Proyecto con un sistema de diseño propio Ya tienes tus componentes Tu librería de componentes
Equipo que trabaja con utility-first Otra filosofía Tailwind CSS
Aprender CSS Te oculta cómo funcionan las cosas Primero CSS puro (como en este curso)

10. Comparación breve con Tailwind CSS

Tailwind CSS es otro framework muy popular, con un enfoque distinto: utility-first. No trae componentes hechos, sino miles de utilidades de bajo nivel con las que construyes todo.

<!-- Bootstrap: componente ya diseñado -->
<button class="btn btn-primary">Guardar</button>

<!-- Tailwind: el diseño se compone con utilidades -->
<button class="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700 focus-visible:outline-2">
  Guardar
</button>
Aspecto Bootstrap 5.3 Tailwind CSS 4
Filosofía Componentes + utilidades Solo utilidades (utility-first)
Componentes listos Sí (navbar, modal, carrusel…) No (hay librerías aparte, gratuitas y de pago)
JavaScript incluido Sí, para componentes interactivos No
Instalación CDN o npm, sin compilar Requiere un paso de compilación (CLI, Vite…)
Personalización Variables CSS y Sass Configuración en CSS (@theme)
Tamaño final Fijo, salvo importación parcial Solo genera las clases que usas
Aspecto por defecto Reconocible si no se personaliza No tiene aspecto propio
Curva de aprendizaje Suave para empezar Hay que conocer bien CSS

💡 Consejo: No hay un ganador absoluto. Bootstrap es imbatible para tener rápido una interfaz completa y coherente; Tailwind brilla en diseños a medida dentro de proyectos con herramientas de construcción. Lo que aprendiste de CSS te sirve para ambos.


11. Ejercicios

Ejercicio 1: Sobrescribir con criterio

Crea una página con una navbar, tres cards y botones. Sin modificar las clases de Bootstrap, crea tus propias clases (.card-servicio, .btn-cta) para dar un estilo distinto. Usa DevTools para comprobar qué reglas ganan y por qué.

Ejercicio 2: Tema con variables CSS

Usando solo variables CSS (con Bootstrap por CDN), cambia la fuente a una de Google Fonts, el radio global, el color de los enlaces y el color de .btn-primary con todos sus estados (hover, active, focus). Comprueba el resultado navegando con el teclado.

Ejercicio 3: Modo oscuro completo

Añade al ejercicio anterior el conmutador claro/oscuro/automático de la sección 4. Revisa la página en modo oscuro y sustituye cualquier clase fija (bg-white, text-dark, bg-light) por su equivalente semántico. Personaliza --bs-body-bg del modo oscuro.

Ejercicio 4: Iconos accesibles

Crea un pie de página con iconos de redes sociales (fuente de Bootstrap Icons) con aria-label correcto, y una lista de características con iconos SVG en línea decorativos (aria-hidden="true"). Compara el peso de la fuente frente al de los SVG en la pestaña Network.

Ejercicio 5: Bootstrap de marca con Sass

Monta un proyecto con npm y Sass. Define $primary, $secondary, la fuente y $border-radius. Añade dos colores al mapa $theme-colors y crea una página de muestra con botones, badges y textos de todos los colores. Compila con npm run css.

Ejercicio 6: Dieta para Bootstrap

Partiendo del ejercicio 5, cambia a importación parcial incluyendo solo lo necesario para una landing (rejilla, utilidades, botones, navbar, cards). Compara el tamaño de css/estilos.css antes y después.

Ejercicio 7: Utilidades propias

Con la API de utilidades, crea: .cursor-* (responsive), .ratio-img-* y una utilidad .tracking-* para letter-spacing con valores tight (-0.02em), normal (0) y wide (0.1em). Úsalas en una página.


Resumen

Concepto Sintaxis / herramienta Para qué
CSS propio Fichero enlazado después de Bootstrap Ajustes puntuales
Clases propias .card-producto, .btn-cta Personalizar sin romper lo demás
Capas @import url(...) layer(bootstrap); Ganar a Bootstrap sin especificidad
Variables globales --bs-body-font-family, --bs-primary, --bs-primary-rgb Tema en tiempo real
Variables de componente --bs-btn-bg, --bs-navbar-color, --bs-card-spacer-x Personalizar un componente
Modo oscuro data-bs-theme="dark" Tema oscuro global o por zonas
Conmutador localStorage + matchMedia Recordar y seguir la preferencia
Clases semánticas bg-body, bg-body-tertiary, text-body-secondary Adaptarse al modo de color
Bootstrap Icons <i class="bi bi-nombre">, SVG, sprite Iconos
Sass: variables $primary: ...; antes de @import Cambiar valores por defecto (!default)
Sass: mapas map-merge($theme-colors, ...) Añadir o quitar colores del tema
Importación parcial functions, variables, maps, mixins + lo que uses Reducir tamaño
API de utilidades $utilities: map-merge($utilities, (...)) Crear o eliminar utilidades
Accesibilidad visually-hidden-focusable, focus-ring, ARIA, contraste Webs para todos
Tailwind CSS Utility-first con compilación Alternativa para diseños a medida