Cómo crear acordeones accesibles con Bootstrap o Tailwind

Cómo crear acordeones accesibles con Bootstrap y Tailwind
Solucionex
19
Nov 25

Los acordeones son un patrón muy común en interfaces web, pero si no se construyen correctamente pueden ser una pesadilla para la accesibilidad web.
En este post vamos a ver cómo hacerlos totalmente accesibles, primero con Bootstrap y después con Tailwind, siguiendo las recomendaciones de WAI-ARIA Authoring Practices.

Principios de accesibilidad web en un acordeón

Para que un acordeón sea accesible debe cumplir con:

  1. Usar botones reales
    Las cabeceras deben ser <button>, no div clicables. Así se garantiza el soporte nativo de teclado.

  2. Relaciones ARIA

    • El botón lleva aria-expanded="true|false".

    • El botón referencia al panel con aria-controls="id-del-panel".

    • El panel tiene role="region", un id único y aria-labelledby="id-del-botón".

  3. Navegación con teclado

    • Tab → mueve entre botones.

    • Enter o Space → abre/cierra el panel (nativo en <button>).

    • Opcional: y → moverse entre cabeceras rápidamente.

Acordeón accesible con Bootstrap 5

Bootstrap ya trae parte del trabajo hecho. Solo hay que mantener los id consistentes:

<div class="accordion" id="accordionExample">
  <div class="accordion-item">
    <h2 class="accordion-header" id="headingOne">
      <button class="accordion-button" type="button"
              data-bs-toggle="collapse"
              data-bs-target="#collapseOne"
              aria-expanded="true"
              aria-controls="collapseOne">
        Sección 1
      </button>
    </h2>
    <div id="collapseOne" class="accordion-collapse collapse show"
         aria-labelledby="headingOne"
         data-bs-parent="#accordionExample">
      <div class="accordion-body">
        Contenido de la sección 1.
      </div>
    </div>
  </div>
</div>

👉 Bootstrap gestiona automáticamente aria-expanded, aria-controls y aria-labelledby.<div class="accordion" id="accordionExample">

Acordeón accesible con Tailwind

Con Tailwind no hay JS pre-hecho, por lo que hay que programar la lógica.

Ejemplo básico con múltiples secciones abiertas

<div class="w-full max-w-md mx-auto">
  <h2>
    <button 
      id="accordion-button-1"
      class="w-full text-left px-4 py-2 font-medium bg-gray-200 rounded focus:outline-none focus:ring"
      aria-expanded="false"
      aria-controls="accordion-panel-1"
    >
      Sección 1
    </button>
  </h2>
  <div 
    id="accordion-panel-1"
    class="hidden px-4 py-2 border"
    role="region"
    aria-labelledby="accordion-button-1"
  >
    Contenido de la sección 1.
  </div>
</div>

<script>
  const btn = document.getElementById("accordion-button-1");
  const panel = document.getElementById("accordion-panel-1");

  btn.addEventListener("click", () => {
    const expanded = btn.getAttribute("aria-expanded") === "true";
    btn.setAttribute("aria-expanded", !expanded);
    panel.classList.toggle("hidden", expanded);
  });
</script>

👉 Aquí cada panel se abre/cierra de forma independiente.

Ejemplo completo con varias secciones y navegación con teclado

<div class="w-full max-w-md mx-auto border rounded">
  <h2>
    <button id="accordion-button-1" class="accordion-btn w-full text-left px-4 py-3 font-medium bg-gray-200 focus:outline-none focus:ring" aria-expanded="false" aria-controls="accordion-panel-1">
      Sección 1
    </button>
  </h2>
  <div id="accordion-panel-1" class="accordion-panel hidden px-4 py-2 border-t" role="region" aria-labelledby="accordion-button-1">
    Contenido de la sección 1.
  </div>

  <h2>
    <button id="accordion-button-2" class="accordion-btn w-full text-left px-4 py-3 font-medium bg-gray-200 focus:outline-none focus:ring" aria-expanded="false" aria-controls="accordion-panel-2">
      Sección 2
    </button>
  </h2>
  <div id="accordion-panel-2" class="accordion-panel hidden px-4 py-2 border-t" role="region" aria-labelledby="accordion-button-2">
    Contenido de la sección 2.
  </div>
</div>

<script>
  const buttons = document.querySelectorAll(".accordion-btn");

  buttons.forEach((btn, index) => {
    const panel = document.getElementById(btn.getAttribute("aria-controls"));

    btn.addEventListener("click", () => {
      const expanded = btn.getAttribute("aria-expanded") === "true";
      btn.setAttribute("aria-expanded", !expanded);
      panel.classList.toggle("hidden", expanded);
    });

    btn.addEventListener("keydown", (e) => {
      let newIndex = null;

      if (e.key === "ArrowDown") {
        e.preventDefault();
        newIndex = (index + 1) % buttons.length;
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        newIndex = (index - 1 + buttons.length) % buttons.length;
      }

      if (newIndex !== null) {
        buttons[newIndex].focus();
      }
    });
  });
</script>

👉 Este acordeón permite abrir varios paneles a la vez y navegar entre botones con y .

Ejemplo estilo Bootstrap: solo un panel abierto

<div class="w-full max-w-md mx-auto border rounded" id="accordion">
  <h2>
    <button id="accordion-button-1" class="accordion-btn w-full text-left px-4 py-3 font-medium bg-gray-200 focus:outline-none focus:ring" aria-expanded="true" aria-controls="accordion-panel-1">
      Sección 1
    </button>
  </h2>
  <div id="accordion-panel-1" class="accordion-panel px-4 py-2 border-t" role="region" aria-labelledby="accordion-button-1">
    Contenido de la sección 1.
  </div>

  <h2>
    <button id="accordion-button-2" class="accordion-btn w-full text-left px-4 py-3 font-medium bg-gray-200 focus:outline-none focus:ring" aria-expanded="false" aria-controls="accordion-panel-2">
      Sección 2
    </button>
  </h2>
  <div id="accordion-panel-2" class="accordion-panel hidden px-4 py-2 border-t" role="region" aria-labelledby="accordion-button-2">
    Contenido de la sección 2.
  </div>
</div>

<script>
  const buttons = document.querySelectorAll(".accordion-btn");

  buttons.forEach((btn, index) => {
    const panel = document.getElementById(btn.getAttribute("aria-controls"));

    btn.addEventListener("click", () => {
      const isExpanded = btn.getAttribute("aria-expanded") === "true";

      // Cerrar todos
      buttons.forEach((b) => {
        b.setAttribute("aria-expanded", "false");
        document.getElementById(b.getAttribute("aria-controls")).classList.add("hidden");
      });

      // Abrir el actual solo si estaba cerrado
      if (!isExpanded) {
        btn.setAttribute("aria-expanded", "true");
        panel.classList.remove("hidden");
      }
    });

    // Navegación con ↑ ↓
    btn.addEventListener("keydown", (e) => {
      let newIndex = null;

      if (e.key === "ArrowDown") {
        e.preventDefault();
        newIndex = (index + 1) % buttons.length;
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        newIndex = (index - 1 + buttons.length) % buttons.length;
      }

      if (newIndex !== null) {
        buttons[newIndex].focus();
      }
    });
  });
</script>

👉 Aquí solo puede haber un panel abierto. El primero comienza expandido.

Conclusión

  • Con Bootstrap ya tienes accesibilidad web básica incluida.

  • Con Tailwind debes implementar la lógica, pero con unos pocos atributos ARIA y algo de JavaScript puedes lograr un acordeón totalmente inclusivo.

  • Para accesibilidad web avanzada, implementa también navegación con flechas y .

Accesibilidad Web
Bootstrap
tailwindcss
Maquetación
Frontend