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:
Usar botones reales
Las cabeceras deben ser<button>, nodivclicables. Así se garantiza el soporte nativo de teclado.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", unidúnico yaria-labelledby="id-del-botón".
Navegación con teclado
Tab→ mueve entre botones.EnteroSpace→ 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↓.