Contents
Introducción
Un acordeón es un patrón de interfaz muy común para ocultar y mostrar contenido. Utilizar las etiquetas semánticas ltdetailsgt y ltsummarygt proporciona una base accesible y con comportamiento nativo en navegadores modernos. Sin embargo, para ofrecer una experiencia completa (animaciones suaves, soporte para lectores de pantalla más antiguos, control por teclado avanzado y respeto a preferencias de movimiento) conviene mejorar el marcado con CSS y un poco de JavaScript progresivo.
Por qué usar details/summary
- Semántica: el navegador ya entiende que hay una sección expandible.
- Accesibilidad: muchos lectores de pantalla reconocen el widget nativo.
- Progresivo: si JavaScript falla, la funcionalidad básica sigue existiendo.
- Sencillo: menos atributos ARIA manuales que con elementos totalmente personalizados.
Consideraciones de accesibilidad
- Asegurar que el ltsummarygt sea alcanzable y reconocible mediante teclado (lo es por defecto, pero personalizaciones pueden romperlo).
- Añadir roles/atributos cuando sea necesario para compatibilidad con ciertos lectores de pantalla: aria-controls en el summary y role=region en el panel pueden ayudar.
- Proveer foco visible y estados de contraste adecuados.
- Respetar prefers-reduced-motion evitando animaciones si el usuario lo solicita.
- Permitir navegación por flechas entre summarios si existen múltiples items (mejora de UX similar a los acordeones de WAI-ARIA).
Marcado HTML recomendado
Usa una estructura clara: cada item del acordeón será un ltdetailsgt con un ltsummarygt y un contenedor para el contenido. Añadimos atributos ARIA para mejorar la interoperabilidad.
lt!-- Estructura base de un item del acordeón --gt
ltdetails class=accordion-item id=acc-1gt
ltsummary class=accordion-summary id=acc-1-summary aria-controls=acc-1-panelgt
Título del panel 1
lt/summarygt
ltdiv id=acc-1-panel class=accordion-panel role=region aria-labelledby=acc-1-summarygt
ltpgtContenido del panel 1. Aquí puede ir HTML rico: imágenes, listas, formularios...lt/pgt
lt/divgt
lt/detailsgt
lt!-- Repetir por cada panel --gt
Notas sobre el marcado
- El atributo aria-controls en el summary apunta al id del panel, facilitando a tecnologías de asistencia saber qué controla.
- El panel usa role=region y aria-labelledby para enlazar con el summary y mejorar la navegación.
- No elimines el comportamiento nativo del summary si aplicas estilos, mantén la capacidad de recibir foco.
Estilos CSS: apariencia y accesibilidad
A continuación un ejemplo de estilos que incluyen icono personalizado, foco visible y adaptación a quotprefers-reduced-motionquot.
/ Estilos básicos del acordeón /
.accordion-item {
border-bottom: 1px solid #e2e2e2
}
/ Summary: apariencia y cursor /
.accordion-summary {
list-style: none / limpia marcador por seguridad /
cursor: pointer
padding: 1rem
display: flex
align-items: center
justify-content: space-between
font-weight: 600
background: #fff
border: none
}
/ Icono personalizado: rotar cuando está abierto /
.accordion-summary::after {
content: ▸ / triángulo simple /
transform: rotate(0deg)
transition: transform 180ms ease
margin-left: 1rem
}
/ Cuando details está [open] gira el icono /
.accordion-item[open] .accordion-summary::after {
transform: rotate(90deg)
}
/ Panel — dejar overflow oculto para animaciones JS /
.accordion-panel {
padding: 0 1rem 1rem 1rem
}
/ Foco visible accesible /
.accordion-summary:focus {
outline: 3px solid #6ca0ff
outline-offset: 2px
}
/ Respetar preferencia de movimiento /
@media (prefers-reduced-motion: reduce) {
.accordion-summary::after {
transition: none
}
}
JavaScript progresivo para animación y mejoras de accesibilidad
El siguiente script no reemplaza al comportamiento nativo lo complementa: añade animación de altura suave (sin animar a/from auto), sincroniza atributos aria-expanded, gestiona navegación por teclado entre summarios y respeta prefers-reduced-motion. Se presenta como ejemplo que puedes adaptar a tu tema de WordPress (por ejemplo, enqueue en un archivo JS).
/ Accordeón accesible con details/summary - mejora JS /
(function () {
const prefersReducedMotion = window.matchMedia((prefers-reduced-motion: reduce)).matches
// Selecciona todos los accordions de la página
const items = Array.from(document.querySelectorAll(.accordion-item))
if (!items.length) return
// Inicializa aria-expanded y panel estilo
items.forEach(item =gt {
const summary = item.querySelector(.accordion-summary)
const panel = item.querySelector(.accordion-panel)
// Asegurar ids únicos
if (!summary.id) summary.id = summary-{Math.random().toString(36).substr(2, 9)}
if (!panel.id) panel.id = panel-{Math.random().toString(36).substr(2, 9)}
summary.setAttribute(aria-controls, panel.id)
panel.setAttribute(aria-labelledby, summary.id)
panel.style.overflow = hidden
// Set aria-expanded según el estado inicial
summary.setAttribute(aria-expanded, item.hasAttribute(open) ? true : false)
// Si el item está cerrado dejamos panel height 0
if (!item.hasAttribute(open)) {
panel.style.maxHeight = 0px
} else {
panel.style.maxHeight = panel.scrollHeight px
}
// Cuando el usuario hace click en summary: animamos la apertura/cierre si no se reduce movimiento
summary.addEventListener(click, (e) =gt {
// Evitar doble ejecución en algunos navegadores: el click sucede antes del toggle nativo
// Usamos setTimeout para dejar que el estado open se actualice y luego animamos
window.setTimeout(() =gt toggleAnimation(item, panel, summary), 0)
})
// Keyboard: manejar Enter/Space para compatibilidad y navegación por flechas
summary.addEventListener(keydown, (e) =gt {
const key = e.key
if (key === key === Spacebar key === Enter) {
// Let native toggle run, aria se actualizará en click handler setTimeout
return
}
// Flechas para moverse al summary anterior/siguiente
if (key === ArrowDown key === ArrowRight) {
e.preventDefault()
focusNextSummary(items, summary)
} else if (key === ArrowUp key === ArrowLeft) {
e.preventDefault()
focusPrevSummary(items, summary)
} else if (key === Home) {
e.preventDefault()
focusSummaryByIndex(items, 0)
} else if (key === End) {
e.preventDefault()
focusSummaryByIndex(items, items.length - 1)
}
})
})
function toggleAnimation(item, panel, summary) {
const isOpen = item.hasAttribute(open)
// Actualizar aria-expanded
summary.setAttribute(aria-expanded, isOpen ? true : false)
if (prefersReducedMotion) {
// Si el usuario prefiere poca animación: simplemente ajustar maxHeight sin transición
panel.style.transition = none
panel.style.maxHeight = isOpen ? panel.scrollHeight px : 0px
// Forzar reflow para aplicar cambio si necesario
panel.offsetHeight
panel.style.transition =
return
}
// Animación basada en medir scrollHeight
if (isOpen) {
// Abrir: animar max-height desde 0 a scrollHeight
panel.style.transition = max-height 300ms ease
panel.style.maxHeight = panel.scrollHeight px
// Limpiar inline style tras finalizar para permitir el redimensionamiento natural
panel.addEventListener(transitionend, function handler() {
// al estar abierto, dejamos maxHeight en none para que el contenido pueda crecer naturalmente
panel.style.maxHeight = none
panel.removeEventListener(transitionend, handler)
})
} else {
// Cerrar: establecer maxHeight = scrollHeight (estado visible), forzar reflow, luego animar a 0
panel.style.transition = max-height 300ms ease
// Si maxHeight era none, primero fijarlo al scrollHeight
if (panel.style.maxHeight === none panel.style.maxHeight === ) {
panel.style.maxHeight = panel.scrollHeight px
// Forzar reflow
panel.offsetHeight
}
// Animar a 0
window.requestAnimationFrame(() =gt {
panel.style.maxHeight = 0px
})
}
}
// Helpers para navegación por teclado
function focusNextSummary(items, currentSummary) {
const summaries = items.map(i =gt i.querySelector(.accordion-summary))
const idx = summaries.indexOf(currentSummary)
const next = summaries[(idx 1) % summaries.length]
next.focus()
}
function focusPrevSummary(items, currentSummary) {
const summaries = items.map(i =gt i.querySelector(.accordion-summary))
const idx = summaries.indexOf(currentSummary)
const prev = summaries[(idx - 1 summaries.length) % summaries.length]
prev.focus()
}
function focusSummaryByIndex(items, index) {
const summaries = items.map(i =gt i.querySelector(.accordion-summary))
if (summaries[index]) summaries[index].focus()
}
// Opcional: abrir un panel si la URL contiene su id como fragmento (deep-linking)
(function handleFragmentOpen(){
const hash = decodeURIComponent(location.hash.replace(#,))
if (!hash) return
const target = document.getElementById(hash)
if (!target) return
const item = target.closest(.accordion-item)
if (item !item.hasAttribute(open)) {
// Abrir el item para mostrar el target
item.setAttribute(open, )
// Actualizar aria en caso de que el script ya haya inicializado
const summary = item.querySelector(.accordion-summary)
if (summary) summary.setAttribute(aria-expanded, true)
// Ajustar panel height inmediatamente
const panel = item.querySelector(.accordion-panel)
if (panel) {
panel.style.maxHeight = panel.scrollHeight px
// Deshacer el maxHeight despues de una animación corta
setTimeout(() =gt { panel.style.maxHeight = none }, 350)
}
// Llevar foco al elemento objetivo para accesibilidad
const focusTarget = item.querySelector(# CSS.escape(hash))
if (focusTarget) focusTarget.focus()
}
})()
})()
Compatibilidad y consideraciones para WordPress
- Encola el script usando wp_enqueue_script con dependencia de jquery si tu tema lo requiere, o preferentemente sin jQuery para mantenerlo ligero.
- Los fragmentos de código de arriba no deben imprimirse directamente en el HTML sin escapado si los colocas en PHP en WordPress usa funciones de salida seguras como wp_add_inline_script o archivos .js separados.
- Si necesitas soportar navegadores muy antiguos que no reconocen ltdetailsgt (ej. IE11), considera una pequeña polyfill o bien reemplazar con un widget aria completo solo donde sea necesario.
- Prueba con lectores de pantalla populares (NVDA, VoiceOver, JAWS) y navegadores móviles para asegurar experiencia consistente.
Buenas prácticas y optimizaciones
- Mantenlo progresivo: el contenido debe ser legible y navegable incluso sin JavaScript.
- No anules comportamiento nativo salvo que sea necesario añade mejoras, no sustituciones innecesarias.
- Respeta la preferencia de movimiento para usuarios con sensibilidad a las animaciones.
- Proporciona foco visible y establece contraste adecuado para accesibilidad visual.
- Prueba el control por teclado: Tab, Enter, Space, Arrow keys, Home/End.
Ejemplo completo (HTML CSS JS reunidos)
Este fragmento combina los ejemplos anteriores. Inserta el CSS en tu hoja de estilos y el JS en un archivo encolado en WordPress.
lt!-- HTML acordeón --gt
ltdetails class=accordion-itemgt
ltsummary class=accordion-summarygtPanel Alt/summarygt
ltdiv class=accordion-panelgt
ltpgtContenido A.lt/pgt
lt/divgt
lt/detailsgt
ltdetails class=accordion-itemgt
ltsummary class=accordion-summarygtPanel Blt/summarygt
ltdiv class=accordion-panelgt
ltpgtContenido B.lt/pgt
lt/divgt
lt/detailsgt
lt!-- Añadir CSS/JS tal y como se muestra en ejemplos separados --gt
Conclusión
Utilizar ltdetailsgt y ltsummarygt como base para un acordeón en WordPress aporta semántica y accesibilidad por defecto. Con unas pequeñas mejoras en CSS y JavaScript obtendrás animaciones suaves, mejor soporte para tecnologías de asistencia y una experiencia teclado-friendly. Integra el JS como mejora progresiva y evita sobrescribir comportamientos nativos salvo que sea imprescindible.
Recursos útiles
|
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |
