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 🙂 |