Introducción
Este tutorial muestra cómo crear un menú hamburguesa accesible usando principalmente CSS, con una mejora opcional mínima en JavaScript para sincronizar atributos ARIA y mejorar la experiencia con teclado y lectores de pantalla. El objetivo es un patrón reutilizable para temas de WordPress: simple, responsivo, accesible y fácil de integrar.
Resumen de la estrategia
- Usar la técnica del checkbox hack para controlar la visibilidad del menú sin JS (input[type=checkbox] oculto label actuando como disparador).
- Proveer elementos accesibles: roles correctos, texto oculto para lectores de pantalla, foco visible y navegación por teclado.
- Opcionalmente, añadir un pequeño script (pocos líneas) que actualice aria-expanded y cierre el menú con Escape para una experiencia completa con lectores de pantalla y teclado.
Consideraciones de accesibilidad antes de empezar
- Semántica: el menú debe estar marcado como navegación (en el ejemplo se usará ltnavgt dentro del código, aunque en este artículo se describe el markup).
- Control mediante teclado: el disparador debe ser focoable y accionable con Enter/Espacio.
- Lectores de pantalla: añadir texto visible solo para lectores de pantalla (visually-hidden) y atributos ARIA como aria-controls y aria-expanded.
- Estados visuales: mantener un indicador de foco claro y suficiente contraste en los elementos interactivos.
Estructura HTML recomendada
Ejemplo mínimo de marcado para el menú hamburguesa. Se utiliza un input checkbox (oculto) y un label que sirve de botón. El menú es un ltulgt con enlaces.
lt!-- Contenedor del encabezado / sitio --gt
ltheader class=site-headergt
ltdiv class=brandgt
lta href=/ class=logogtMi Sitiolt/agt
lt/divgt
lt!-- Checkbox oculto que controla la visibilidad del menú --gt
ltinput id=menu-toggle class=menu-toggle type=checkbox aria-hidden=true /gt
lt!-- Label que actúa como botón hamburguesa --gt
ltlabel for=menu-toggle class=hamburger role=button aria-controls=primary-menu tabindex=0gt
ltspan class=hamburger-boxgt
ltspan class=hamburger-innergtlt/spangt
lt/spangt
ltspan class=sr-onlygtAbrir menú principallt/spangt
lt/labelgt
ltnav id=site-navigation class=main-navigation aria-label=Menú principalgt
ltul id=primary-menu class=menugt
ltligtlta href=/categoria-1gtCategoría 1lt/agtlt/ligt
ltligtlta href=/categoria-2gtCategoría 2lt/agtlt/ligt
ltligtlta href=/contactogtContactolt/agtlt/ligt
lt/ulgt
lt/navgt
lt/headergt
Notas sobre el HTML
- El input tiene aria-hidden=true porque es un truco puramente de presentación el label actúa como control. Otra opción más semántica es usar un ltbuttongt y JavaScript mínimo, preferible si se desea mejor ARIA.
- El texto con clase sr-only es visible para lectores de pantalla y describe la acción del botón.
- El label tiene tabindex=0 y role=button para que sea focoable y anunciado como botón por lectores de pantalla cuando se usa el input hack.
CSS: estilos base, hamburguesa y comportamiento responsive
Estilos esenciales: ocultar checkbox, dibujar la hamburguesa, controlar la visibilidad del menú con el selector :checked y asegurar foco visible.
/ Reseteo mínimo /
.menu-toggle { position: absolute left: -9999px }
/ Texto solo para lectores de pantalla /
.sr-only {
position: absolute !important
height: 1px width: 1px
overflow: hidden
clip: rect(1px, 1px, 1px, 1px)
white-space: nowrap
}
/ Estilos del botón hamburguesa /
.hamburger {
display: inline-block
cursor: pointer
padding: 0.5rem
border-radius: 4px
transition: background .15s
user-select: none
}
.hamburger:focus {
outline: 3px solid #0a84ff / foco visible /
outline-offset: 3px
}
.hamburger-box { display: inline-block width: 30px height: 20px position: relative }
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after {
display: block
background-color: #111
height: 2px
border-radius: 2px
position: absolute
left: 0
right: 0
transition: transform .25s ease, opacity .25s ease
}
.hamburger-inner { top: 50% transform: translateY(-50%) }
.hamburger-inner::before { content: top: -8px }
.hamburger-inner::after { content: bottom: -8px }
/ Menú oculto por defecto en pantallas pequeñas /
.main-navigation { display: none }
.main-navigation .menu { list-style: none margin: 0 padding: 0 }
.main-navigation .menu li { margin: 0 }
/ Cuando el checkbox está marcado, mostrar el menú /
.menu-toggle:checked .hamburger .main-navigation {
display: block
}
/ Animación de transformación a X /
.menu-toggle:checked .hamburger .hamburger-inner {
transform: rotate(45deg)
}
.menu-toggle:checked .hamburger .hamburger-inner::before {
transform: translateY(8px) rotate(90deg)
}
.menu-toggle:checked .hamburger .hamburger-inner::after {
opacity: 0
}
/ Responsive: en pantallas grandes mostrar el menú siempre /
@media (min-width: 768px) {
.hamburger { display: none }
.main-navigation { display: block !important }
.main-navigation .menu { display: flex gap: 1rem align-items: center }
}
Explicación del CSS
- El selector combinado .menu-toggle:checked .hamburger .main-navigation depende del orden en el DOM: input, label, nav. Cuando el checkbox está marcado, el menú se muestra.
- Las transformaciones sobre .hamburger-inner hacen la animación de la hamburguesa a una X.
- En pantallas grandes el menú se muestra siempre y el botón se oculta.
Mejoras de accesibilidad (JS mínimo opcional)
Con la técnica CSS-only, los atributos ARIA como aria-expanded no se actualizan automáticamente. Para que los lectores de pantalla informen correctamente el estado y para gestionar teclas como Escape, se recomienda un pequeño script que sincronice el estado y gestione el cierre con Escape.
/ Script opcional: sincroniza aria-expanded y cierra con Escape /
document.addEventListener(DOMContentLoaded, function () {
var toggle = document.getElementById(menu-toggle)
var label = document.querySelector(label[for=menu-toggle])
var menu = document.getElementById(primary-menu)
if (!toggle !label !menu) return
// Inicializar aria-expanded
label.setAttribute(aria-expanded, toggle.checked ? true : false)
// Al cambiar el checkbox sincroniza aria-expanded y aria-hidden
toggle.addEventListener(change, function () {
label.setAttribute(aria-expanded, toggle.checked ? true : false)
menu.setAttribute(aria-hidden, toggle.checked ? false : true)
})
// Permitir activar el label con Enter/Espacio cuando se enfoca
label.addEventListener(keydown, function (e) {
if (e.key === Enter e.key === ) {
e.preventDefault()
toggle.checked = !toggle.checked
toggle.dispatchEvent(new Event(change))
label.focus()
}
})
// Cerrar con Escape si el menú está abierto
document.addEventListener(keydown, function (e) {
if (e.key === Escape toggle.checked) {
toggle.checked = false
toggle.dispatchEvent(new Event(change))
label.focus()
}
})
})
Por qué este JS es útil
- Actualiza aria-expanded y aria-hidden de forma coherente.
- Asegura que activar con teclado funcione igual que hacer clic.
- Proporciona una manera de cerrar el menú con Escape, lo esperado por muchos usuarios de teclado y lectores de pantalla.
Integración en WordPress
Para integrar el HTML en un tema de WordPress puedes registrar la ubicación del menú y renderizarlo con wp_nav_menu(). A continuación un ejemplo simplificado para functions.php y la plantilla del header.
/ functions.php: registrar la ubicación del menú /
function theme_register_menus() {
register_nav_menus( array(
primary => Menú Principal,
) )
}
add_action( after_setup_theme, theme_register_menus )
/ header.php: implementar el markup (simplificado) /
ltheader class=site-headergt
ltdiv class=brandgtlta href=/ class=logogtlt?php bloginfo(name) ?gtlt/agtlt/divgt
ltinput id=menu-toggle class=menu-toggle type=checkbox aria-hidden=true /gt
ltlabel for=menu-toggle class=hamburger role=button aria-controls=primary-menu tabindex=0gt
ltspan class=hamburger-boxgtltspan class=hamburger-innergtlt/spangtlt/spangt
ltspan class=sr-onlygtAbrir menú principallt/spangt
lt/labelgt
ltnav id=site-navigation class=main-navigation aria-label=Menú principalgt
lt?php
wp_nav_menu( array(
theme_location => primary,
menu_id => primary-menu,
container => false,
items_wrap => ltul id=%1s class=menugt%3slt/ulgt,
) )
?gt
lt/navgt
lt/headergt
Enqueue de estilos y script
En functions.php añade el CSS y, si usas la mejora JS, el script de forma adecuada con dependencias y encolado.
function theme_enqueue_assets() {
wp_enqueue_style( theme-main, get_stylesheet_uri() )
wp_enqueue_script( theme-menu, get_template_directory_uri() . /js/menu-accessible.js, array(), 1.0, true )
}
add_action( wp_enqueue_scripts, theme_enqueue_assets )
Pruebas y comprobaciones
- Probar navegación por teclado: Tab para llegar al botón, Enter/Espacio para abrir/cerrar, Escape para cerrar (si usas el JS).
- Probar con lectores de pantalla (NVDA, VoiceOver, TalkBack): verificar que el control anuncia estado y el menú es navegable.
- Comprobación responsive: cambiar ancho de ventana para ver la transición entre menú hamburguesa y menú habitual en desktop.
- Accesibilidad visual: contraste y foco visible en el botón y enlaces.
- Validar el HTML/CSS y comprobar que la orden del DOM (input label nav) no rompe el layout y funciona con selectores adyacentes.
Resumen y recomendaciones finales
La técnica con checkbox permite construir un menú hamburguesa sin depender de JavaScript, pero para una experiencia plenamente accesible (estado ARIA correcto, mejor manejo del teclado y cierre con Escape) es recomendable añadir un pequeño script. Si prefieres una solución estrictamente semántica, utiliza un ltbuttongt y controla la apertura con JavaScript desde el inicio —esa es la opción más robusta desde el punto de vista de accesibilidad. Integra CSS y JS mediante las funciones de WordPress para mantener compatibilidad con cachés y child themes.
Implementa pruebas reales con usuarios o herramientas de evaluación (axe, Lighthouse, NVDA) y ajusta colores, tamaños de hit area y tiempos de transición para una experiencia óptima.
Leave a Reply