Como construir un menu desplegable con JavaScript
Frontend

Como construir un menu desplegable con JavaScript

4 October 2026 · 8 min de lectura
Home / Frontend / Como construir un menu desplegable con JavaScript
Categorías:

Introduccion a los menus desplegables en la web

Los menus desplegables son uno de los patrones de interfaz mas utilizados en aplicaciones web. Permiten agrupar acciones o enlaces relacionados sin saturar el espacio visual disponible. Aparecen en barras de navegacion, selectores de formularios, menus de usuario y paneles de acciones contextuales. Construir uno desde cero con HTML, CSS y JavaScript ofrece un control total sobre el comportamiento, la apariencia y la accesibilidad.

En 2026 las expectativas de calidad han aumentado. Un dropdown moderno no solo debe abrirse y cerrarse con el raton; debe responder al teclado, comunicar su estado a los lectores de pantalla mediante atributos ARIA, cerrarse al pulsar Escape o al hacer clic fuera, y mantener un foco predecible. Este articulo detalla el proceso completo de construccion de un menu desplegable de navegacion, desde el marcado semantico hasta las mejoras de accesibilidad y las animaciones suaves.

El enfoque se basa en vanilla JavaScript para que los conceptos queden claros y sean transferibles a cualquier framework. Al finalizar, el lector dispondra de un componente reutilizable y de una comprension solida de los principios que lo hacen robusto.

Estructura HTML semantica del dropdown

El punto de partida es un marcado claro. Para un menu de navegacion el patron recomendado es un boton que controla la visibilidad de una lista de enlaces. Evitar el uso de enlaces como disparadores mejora la semantica: un enlace navega, un boton cambia el estado de la interfaz.

<div class="dropdown">
    <button
        type="button"
        class="dropdown-toggle"
        id="dropdown-btn"
        aria-expanded="false"
        aria-controls="dropdown-menu"
        aria-haspopup="true"
    >
        Opciones
        <span class="arrow" aria-hidden="true"></span>
    </button>
    <ul class="dropdown-menu" id="dropdown-menu" hidden>
        <li><a href="#crear">Crear nuevo</a></li>
        <li><a href="#borradores">Todos los borradores</a></li>
        <li><a href="#mover">Mover a</a></li>
        <li><a href="#perfil">Configuracion de perfil</a></li>
        <li><a href="#notificaciones">Notificaciones</a></li>
        <li><a href="#ajustes">Ajustes</a></li>
    </ul>
</div>

Los atributos aria-expanded, aria-controls y aria-haspopup comunican la relacion entre el boton y el menu a las tecnologias de asistencia. El atributo hidden mantiene el menu cerrado incluso antes de que se ejecute el JavaScript, evitando un destello de contenido.

Estilos base y posicionamiento

El contenedor del dropdown se posiciona de forma relativa para que el menu absoluto se ancle correctamente debajo del boton. Las variables CSS facilitan el mantenimiento de colores, sombras y radios de borde.

:root {
    --dropdown-bg: #ffffff;
    --dropdown-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
    --dropdown-radius: 8px;
    --accent: #166e67;
    --gap: 0.5rem;
}

.dropdown {
    position: relative;
    display: inline-block;
}

.dropdown-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--gap);
    padding: 0.6rem 1rem;
    background: var(--dropdown-bg);
    border: 1px solid #e2e8f0;
    border-radius: var(--dropdown-radius);
    cursor: pointer;
    font-size: 0.95rem;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}

.dropdown-menu {
    position: absolute;
    top: calc(100% + 0.35rem);
    left: 0;
    min-width: 220px;
    margin: 0;
    padding: 0.35rem 0;
    list-style: none;
    background: var(--dropdown-bg);
    border-radius: var(--dropdown-radius);
    box-shadow: var(--dropdown-shadow);
    z-index: 100;
}

.dropdown-menu a {
    display: block;
    padding: 0.65rem 1rem;
    color: #1a202c;
    text-decoration: none;
}

.dropdown-menu a:hover,
.dropdown-menu a:focus-visible {
    background: var(--accent);
    color: #fff;
}

El uso de focus-visible garantiza que el indicador de foco solo aparezca en navegacion por teclado, evitando anillos indeseados en clics de raton.

Ocultacion y animacion del menu

En lugar de display: none, que no permite transiciones, se combinan opacity y visibility. Esta tecnica, utilizada por interfaces como la de GitHub, permite animar la aparicion y desaparicion.

.dropdown-menu {
    /* ...estilos anteriores... */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.35rem);
    transition:
        opacity 0.15s ease,
        transform 0.15s ease,
        visibility 0.15s;
}

.dropdown-menu.show {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.arrow {
    display: inline-block;
    transition: transform 0.2s ease;
}

.arrow.open {
    transform: rotate(180deg);
}

La clase show se anadira y eliminara mediante JavaScript. El desplazamiento vertical sutil refuerza la sensacion de que el menu emerge desde el boton.

Logica de apertura y cierre con JavaScript

El nucleo del comportamiento consiste en alternar la clase show y actualizar el atributo aria-expanded. Tambien se rota la flecha para ofrecer feedback visual.

const btn = document.getElementById("dropdown-btn");
const menu = document.getElementById("dropdown-menu");
const arrow = btn.querySelector(".arrow");

function openDropdown() {
    menu.classList.add("show");
    menu.hidden = false;
    btn.setAttribute("aria-expanded", "true");
    arrow.classList.add("open");
}

function closeDropdown() {
    menu.classList.remove("show");
    menu.hidden = true;
    btn.setAttribute("aria-expanded", "false");
    arrow.classList.remove("open");
}

function toggleDropdown() {
    const isOpen = btn.getAttribute("aria-expanded") === "true";
    if (isOpen) {
        closeDropdown();
    } else {
        openDropdown();
    }
}

btn.addEventListener("click", (event) => {
    event.stopPropagation();
    toggleDropdown();
});

La llamada a stopPropagation evita que el clic en el boton se propague al documento y cierre inmediatamente el menu que se acaba de abrir.

Cierre al hacer clic fuera del componente

La mayoria de los usuarios esperan que el menu se cierre al interactuar con cualquier otra parte de la pagina. Se escucha el clic en el documento y se comprueba si el objetivo esta fuera del dropdown.

document.addEventListener("click", (event) => {
    const isClickInside =
        btn.contains(event.target) || menu.contains(event.target);
    if (!isClickInside && btn.getAttribute("aria-expanded") === "true") {
        closeDropdown();
    }
});

Esta comprobacion es eficiente y funciona incluso cuando existen multiples dropdowns en la misma pagina, siempre que cada uno gestione su propio estado.

Soporte de teclado basico

Un dropdown accesible debe abrirse y cerrarse con el teclado. Las teclas Enter y Espacio activan el boton de forma nativa. Escape debe cerrar el menu y devolver el foco al boton.

btn.addEventListener("keydown", (event) => {
    if (
        event.key === "Escape" &&
        btn.getAttribute("aria-expanded") === "true"
    ) {
        closeDropdown();
        btn.focus();
    }
});

menu.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
        closeDropdown();
        btn.focus();
    }
});

Para una navegacion mas completa se pueden anadir flechas arriba y abajo que muevan el foco entre los elementos del menu. En un menu de navegacion simple con enlaces nativos, el tabulador ya permite recorrer las opciones de forma natural.

Gestion del foco y atrapamiento opcional

En menus complejos a veces se desea mantener el foco dentro del panel mientras esta abierto. Para un dropdown de navegacion habitual no es estrictamente necesario, pero el patron es util conocerlo.

function getFocusableElements(container) {
    return container.querySelectorAll(
        'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
    );
}

// Ejemplo de ciclo de foco (opcional)
menu.addEventListener("keydown", (event) => {
    if (event.key !== "Tab") return;
    const focusables = [...getFocusableElements(menu)];
    if (focusables.length === 0) return;

    const first = focusables[0];
    const last = focusables[focusables.length - 1];

    if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
    }
});

Este tipo de atrapamiento se recomienda principalmente en dialogos modales. En un disclosure de navegacion suele ser preferible permitir que el tabulador salga del menu de forma natural.

Multiples dropdowns en la misma pagina

Cuando coexisten varios menus, abrir uno debe cerrar los demas. Una solucion sencilla consiste en seleccionar todos los contenedores y cerrar aquellos que no son el actual.

const allDropdowns = document.querySelectorAll(".dropdown");

function closeAllExcept(currentBtn) {
    allDropdowns.forEach((dropdown) => {
        const otherBtn = dropdown.querySelector(".dropdown-toggle");
        const otherMenu = dropdown.querySelector(".dropdown-menu");
        if (
            otherBtn !== currentBtn &&
            otherBtn.getAttribute("aria-expanded") === "true"
        ) {
            otherMenu.classList.remove("show");
            otherMenu.hidden = true;
            otherBtn.setAttribute("aria-expanded", "false");
            const otherArrow = otherBtn.querySelector(".arrow");
            if (otherArrow) otherArrow.classList.remove("open");
        }
    });
}

btn.addEventListener("click", (event) => {
    event.stopPropagation();
    closeAllExcept(btn);
    toggleDropdown();
});

Este enfoque mantiene la logica local a cada instancia y evita dependencias globales complejas.

Consideraciones de accesibilidad en 2026

Las guias WAI-ARIA distinguen entre un menu de aplicacion (role=“menu”) y un disclosure de navegacion. Para la mayoria de los menus de sitio web el patron de disclosure (boton + lista de enlaces) es el mas adecuado y no requiere role=“menu” ni role=“menuitem”. Anadir roles de menu implica implementar un conjunto completo de interacciones de teclado propias de aplicaciones, lo que aumenta la complejidad sin beneficio claro en navegacion ordinaria.

Los puntos esenciales de accesibilidad son:

  • Usar un boton real como disparador.
  • Mantener sincronizados aria-expanded y el estado visual.
  • Permitir cierre con Escape.
  • Conservar el orden de tabulacion natural.
  • Ofrecer un indicador de foco visible.
  • Asegurar contraste suficiente en estados hover y focus.

Probar el componente con un lector de pantalla y solo con teclado revela rapidamente omisiones que no se detectan con el raton.

Variantes de diseno y posicionamiento

En pantallas pequenas o cuando el boton esta cerca del borde derecho, el menu puede desbordarse. Se puede detectar el espacio disponible y aplicar una clase que alinee el menu a la derecha.

function adjustPosition() {
    const rect = btn.getBoundingClientRect();
    const spaceRight = window.innerWidth - rect.right;
    if (spaceRight < 220) {
        menu.style.left = "auto";
        menu.style.right = "0";
    } else {
        menu.style.left = "0";
        menu.style.right = "auto";
    }
}

btn.addEventListener("click", () => {
    if (btn.getAttribute("aria-expanded") === "false") {
        adjustPosition();
    }
});

Otras variantes incluyen menus de ancho completo en movil, menus con iconos, separadores y grupos de opciones. La estructura base presentada se adapta facilmente a estos casos.

Integracion con frameworks y librerias

Aunque el ejemplo utiliza vanilla JavaScript, los mismos principios se aplican en React, Vue o Svelte. En React el estado abierto se gestiona con useState y los atributos ARIA se enlazan de forma declarativa. Las librerias de componentes accesibles (Headless UI, Radix, Reach UI, etc.) implementan estos patrones internamente y son una buena opcion cuando el tiempo de desarrollo es limitado.

Comprender la implementacion manual permite evaluar la calidad de cualquier libreria y diagnosticar problemas de accesibilidad cuando aparecen.

Errores comunes a evitar

Uno de los fallos mas frecuentes es utilizar un enlace como disparador. Otro es olvidar actualizar aria-expanded, lo que deja a los lectores de pantalla con informacion incorrecta. Confiar unicamente en hover excluye a usuarios de teclado y de pantallas tactiles. Por ultimo, animar con display: none impide las transiciones y produce un comportamiento abrupto.

La comprobacion sistematica de estos puntos antes de dar por terminado el componente eleva notablemente su calidad percibida.

Conclusiones

Construir un menu desplegable con JavaScript es un ejercicio excelente para consolidar el manejo del DOM, los eventos, las clases CSS y los fundamentos de accesibilidad. Un componente bien disenado combina marcado semantico, estados ARIA sincronizados, cierre predecible y soporte de teclado.

Las tecnicas presentadas (posicionamiento relativo-absoluto, transiciones de opacity y visibility, stopPropagation, deteccion de clics exteriores y actualizacion de aria-expanded) forman parte del repertorio basico de cualquier desarrollador frontend. Dominarlas permite crear interfaces interactivas solidas y prepara el terreno para componentes mas complejos como comboboxes, menubars y dialogos modales.

El resultado final no es solo un dropdown funcional, sino una pieza de interfaz respetuosa con todos los usuarios y alineada con las practicas recomendadas en 2026.