A class toggle is enough to show and hide a panel. The W3C’s ARIA Authoring Practices Guide (2025) still points most websites somewhere else, though. It names the disclosure pattern, a button that toggles a list of links, as a better fit than the ARIA menu role.

Dropdown, mega menu, off-canvas and accordion layouts all get built this way, and whichever one you pick ends up judged on how it handles keyboard and touch input, and on whether its links stay crawlable.

What are JavaScript menus, and how do they differ from CSS-only menus?

Script is the difference. It takes over open and close state, event handling, keyboard focus and the ARIA attributes. Take it away and you have a pure CSS dropdown menu, which handles :hover and :focus-within and little else.

The HTML holds the links and CSS controls visibility, with script sitting on top to hold the state. It doesn’t take much code either. A basic open and close toggle is a few lines of plain JavaScript.

CSS can show a submenu on hover or focus, style the open and closed states, and animate the panel with transitions. That’s about where it stops.

Everything else needs script. Escape should close the panel and send focus back to its trigger button. aria-expanded has to be updated whenever the state changes. A click outside the menu should close it, and if you only want one submenu open at a time, that logic is yours to write.

The select-based jump menu looks similar but isn’t really a navigation menu. It’s a form control that loads the chosen URL from a change handler, so it stops working when scripting fails.

Dropdown, mega, hamburger, accordion and context menus: which layout each one fits

The right pick depends on how many links each top-level item holds and how much screen width is left over.

TypeFitsUsual trigger
DropdownOne short list of links per top-level itemClick on a button
Mega menuLarge sites with grouped categoriesClick, tap or delayed hover
Hamburger (off-canvas)Narrow viewportsTap on a toggle button
AccordionSidebars and stacked mobile listsClick on a header button
Context menuActions on a selected item in an appRight click

Most builds start with a basic dropdown, a button that opens a list of links, driven by one state value. The other types reuse that toggle logic with more markup around it.

Mega menus change the trade-off. Nielsen Norman Group (Nielsen and Li, 2017) describes them as large two-dimensional panels with grouped options, all visible at once. A regular dropdown hides most choices and can’t group them without workarounds.

The hamburger menu puts all navigation behind one toggle button, so its whole job is state. The button flips aria-expanded and the off-canvas panel slides in. Save it for widths where the links no longer fit.

Context menus sit outside site navigation. They open on right click and belong in application-style interfaces, which is the one place the ARIA menu role fits.

How to build a dropdown menu with JavaScript

Start with a button and a list of links. Keep the open state in aria-expanded and flip it from a single click handler. CSS reads the same attribute, so the visual state and the announced state stay in sync.

Markup and state

Plain HTML is enough here: a nav element with a list inside it, and a button sitting directly before the sub-list it controls.

State can live in a few places. classList.toggle adds or removes a class such as show. It’s simple, but a screen reader announces nothing when the class changes.

The hidden attribute removes the panel from rendering and from the accessibility tree. aria-expanded is a true or false value on the button, announced as expanded or collapsed, and CSS can read it with an attribute selector.

Make aria-expanded the single source of truth.

The W3C APG disclosure example does exactly this, and it styles both states with attribute selectors such as [aria-expanded=”true”].

Toggle script

  1. Select every button that carries aria-expanded and aria-controls.
  2. Attach one click listener to the nav element instead of one per button (event delegation).
  3. On click, read the button’s current aria-expanded value and flip it.
  4. Set or remove the hidden attribute on the controlled list so it matches.
  5. Close any other open dropdown before opening the new one.
  6. Close on Escape and when focus leaves the nav, and send focus back to the button.

The APG disclosure navigation example (revised January 2026) keeps its logic in a single file, disclosureMenu.js. Read it beside your own version, but note that the W3C labels the code as illustrative, not production-ready, and asks for testing with real assistive technology.

Which events should open and close a menu?

Click opens a menu on every input type, so bind it first. Hover is an add-on for desktop, and it needs a delay.

According to Baymard Institute (2023), a hover delay of 300 to 500 milliseconds removes most accidental triggering, and 60% of sites in its e-commerce benchmark still skip it.

Macy’s applies the delay, so the pointer can cross the main navigation without opening anything. John Lewis opens panels instantly, and the wrong category is easy to trigger.

Nielsen Norman Group’s timing guideline lands in the same range. Wait 0.5 seconds before showing hover content, display it within 0.1 seconds, and hold it until the pointer has been outside for 0.5 seconds. The closing grace period exists for the diagonal problem, where the pointer path briefly leaves the active area on its way to a menu item.

Click toggles the menu for mouse, touch and keyboard, because a button fires click on Enter and Space. mouseenter and mouseleave handle hover opening, always behind a delay timer. keydown covers Escape to close the panel, plus arrow keys if you add them, and focusout closes the panel once focus leaves the nav.

Hover-triggered panels fall under WCAG 2.2 success criterion 1.4.13 (Level AA). The panel has to be dismissible without moving the pointer. The pointer also needs to be able to travel onto the panel, and the panel should stay visible until the trigger is removed or the user dismisses it.

Touch screens have no hover at all. A hover-only menu gives those users no way in.

A click outside the menu closes it, and so does Escape. Add focus leaving the nav and you’ve covered most cases.

Disclosure pattern or menubar: which one fits site navigation?

Use the disclosure pattern for site navigation. The W3C APG recommends it for most websites because few sites need the extra keyboard behavior that the menubar and menu roles demand.

Both patterns rely on ARIA, but they make different promises to assistive technology. A menu role tells a screen reader the widget behaves like a desktop application menu, so users expect arrow keys everywhere.

KeyDisclosure navigationMenubar
TabMoves through every button and open linkOne stop for the whole bar (roving tabindex)
Enter or SpaceToggles a dropdown or follows a linkOpens a submenu or activates the item
EscapeCloses the dropdown, focus returns to its buttonCloses the submenu, focus returns to the parent item
Arrow keysOptionalRequired: Left and Right move along the bar, Up and Down open and move through submenus

Disclosure pattern

See the Pen
Stripe.com Style Dropdown Menu
by Rian Ariona (@ariona)
on CodePen.

Each top-level item is a button, and the dropdown is a nested list of ordinary links.

The button carries aria-expanded set to true or false, and aria-controls points to the dropdown’s id. Put an aria-label on the nav so the landmark has a name.

Leave aria-haspopup off the buttons. The attribute announces a popup with the menu role, a promise this pattern does not keep (MDN states plainly that the menu role is not for site navigation).

As of the January 2026 revision of the APG example, Escape closes an open dropdown and returns focus to its button. The APG calls this behavior necessary to meet criterion 1.4.13.

Menubar pattern

Menu roles go wrong a lot in the wild. WebAIM’s 2025 web accessibility audit of one million home pages found role=”menu” on 4.5% of them, and 35% of those menus created barriers because the required markup and interactions were missing (WebAIM Million, 2025).

That’s a lot to ask of a widget. Arrow keys have to move focus, exactly one item can sit at tabindex 0 in a roving tabindex, and typing a character should jump to a matching item. Miss any one and the widget breaks its own promise.

The APG still publishes a menubar navigation example, with a caution on top. Practitioners pushed for lighter options. Per Evinced’s comparison, Sarah Higley’s “Disclosure for Navigation Menus” was accepted into the APG as an alternative, and Adrian Roselli’s link plus disclosure widget approach is the third pattern in that comparison.

A menubar fits a real application menu, such as the APG’s editor menubar example. It does not fit a marketing site.

Button or link for each item

If an item opens a submenu, make it a button. If it goes to a URL, make it an anchor with an href.

A parent that is both a page and a section needs both. The APG documents this as the disclosure navigation menu with top-level links, which gives you a real link plus a separate disclosure button beside it.

Nielsen Norman Group reached the same layout from the usability side in 2017. Make each top-level choice clickable and send it to a regular page that lists every dropdown option.

Google Search Central adds the crawling angle. Links are discovered only as a elements with an href. Dropdown links stay crawlable as long as they are real anchors in the HTML, but a parent that is only a button gives crawlers no URL for the section itself.

How to switch a menu between desktop and mobile layouts

See the Pen
Nav Menu Animation
by Álvaro (@alvarotrigo)
on CodePen.

Pick the breakpoint where the links stop fitting in one row, then drive the CSS and the script from that same condition. When the condition flips, reset every open state so a resize never leaves a panel half open.

The stylesheet’s media queries switch the layout, and matchMedia hands the same condition to script. It returns a MediaQueryList with a matches property for the current state and a change event that fires when the viewport crosses the line.

There’s a syntax trap here. The feature must sit in parentheses, as in (width <= 600px), and without them the query string is invalid (MDN).

Writing the stacked, closed layout first and adding the horizontal bar at the breakpoint follows a mobile-first approach, which keeps the base styles short.

Each time the viewport crosses the breakpoint, set aria-expanded back to false on every toggle button and put the hidden attribute back on each closed panel. Remove any body scroll lock, and if the panel that just closed held focus, return it to the trigger.

The off-canvas panel is where the inert attribute earns its place. A closed panel marked inert leaves the tab order and the accessibility tree, and the page behind an open one can be marked inert too (supported across browsers since April 2023 and Baseline widely available, per MDN).

MDN adds a warning that inert gives no visual cue by default, so style the dimmed state yourself.

WCAG 2.2 success criterion 2.5.8 (Level AA) sets a 24 by 24 CSS pixel minimum for pointer targets, with a spacing exception. The W3C’s own dropdown example passes at 24 pixel items and fails at 18.

MDN recommends 44 by 44 CSS pixels for buttons, so treat 24 as the floor and 44 as the target for mobile menu items.

Are JavaScript menus crawlable, and do they slow interaction?

Yes to both, with conditions. The anchors need to reach the rendered HTML, and a lean click handler adds almost no delay.

Link discovery

Google runs JavaScript with an evergreen version of Chromium, and Search Central (revised March 2026) says injecting links into the DOM with script is fine.

Rendering still sits in its own queue. A page may wait a few seconds there, or longer, so anchors in the initial HTML are found at crawl time while injected ones wait for the renderer.

Anchors in the server HTML are the safest, with script adding only the toggles. Anchors injected on load, before anyone interacts, also work, and so does an anchor that carries both an href and an onclick (Google lists it as parseable).

Some setups are riskier.

  • javascript: URLs in the href, which Google marks as not recommended
  • Hash routes such as #/products, where the History API is the better route
  • Submenu anchors fetched only after a hover or click, so verify those in the URL Inspection tool

A mega menu that loads its panels through an Ajax call on hover sits in that riskier group.

Google also notes that server-side or pre-rendering is still a good idea because not every bot runs JavaScript. A menu that keeps the whole site’s website navigation inside script has no fallback for those bots.

Interaction latency

Interaction to Next Paint rates a page good at 200 milliseconds or less and poor above 500, measured at the 75th percentile of page loads (web.dev, 2025).

INP counts clicks, taps and key presses. Hovering is not observed, so a hover-only menu slips past the metric while still failing touch users.

web.dev names a navigation menu opening as the kind of visual feedback the metric waits for. It times the wait until the next paint, not the network fetches that follow, so paint the open state first and load content after.

Long tasks already running when the tap lands will push a menu click past the limit. So will layout reads mixed between style writes (layout thrashing), an oversized DOM, which is common in mega menu panels, and a fetch awaited before the state changes.

Vanilla JavaScript, jQuery, frameworks and native elements compared

See the Pen
jq bs4 megamenu and sidemenu mobile
by Axelaredz (@axelaredz)
on CodePen.

Choose the smallest option that covers the menu’s job. Plain script is enough for a one-level dropdown. A headless library earns its weight in a component-based app, and native elements are hard to beat when open and closed state is all the menu needs.

OptionWhat it providesAccessibility modelMain limit
Vanilla JavaScriptState, keys and dismissal you writeAs built; the APG disclosure example is the referenceYou own every edge case
Bootstrap 5.3 dropdownClick toggle, Esc and arrow keysNo ARIA menu roles addedPopper required outside navbars
Alpine.jsx-data, x-show and @click.outside in markupNone in the starter examplearia-expanded added by hand
Radix UI Navigation MenuReact primitive with managed focus, 11.89 kBAvoids the menu roleReact only
details and Popover APINative open state, no toggle scriptBrowser-suppliedPopovers are non-modal; details has no built-in animation

The Bootstrap dropdown plugin is the ready-made route, and its documentation is blunt about what it does. It toggles on click and never on hover, which the docs call an intentional design decision. Popper handles positioning except inside a navbar, and the autoClose option (true, false, inside or outside) decides what dismisses the panel.

Alpine’s starter dropdown is a handful of directives. An x-data flag holds the state, @click flips it, x-show hides the panel, and @click.outside closes it.

The docs example sets no aria-expanded, so bind it yourself.

Radix keeps Navigation Menu apart from its Dropdown Menu and Menubar components. Its Navigation Menu page says it skips the menu role on purpose, because composite focus management and first-character navigation are unnecessary for website navigation.

Headless UI lists Dropdown Menu beside Disclosure and Popover. Its Menu focuses the first item on open and, with the modal prop on by default, traps focus and makes the rest of the page inert, so for main navigation Disclosure is the closer fit.

Floating UI solves positioning and viewport collisions, and its React package adds hooks for hover, focus, click and dismiss. It is modular and tree-shakeable, so bundle size tracks what you import.

jQuery only makes sense where the site already ships it. Bootstrap 5.3’s plugin starts from plain JavaScript or data attributes, so a new build gains nothing by adding jQuery for one dropdown.

Native elements cover more than most builds assume. The details element has been Baseline widely available since January 2020 and keeps its state in an open attribute, and giving grouped elements the same name turns them into an exclusive accordion without script.

The Popover API (Baseline newly available since January 2025) turns a popovertarget button and a popover element into a toggle, and the browser adds the aria-expanded relationship for you.

When a JavaScript menu fails or is the wrong choice

A JavaScript menu fails when its script never runs, when browsers disagree about focus or taps, when the panel sits under another layer, or when the site never needed a dropdown at all.

Failure conditions

  • If the links exist only after script runs, a failed load leaves an empty navigation bar. Keep the anchors in the HTML and let script add the toggles.
  • MDN states that Safari does not focus a button on click, by design, so a close rule built only on focusout misses clicks there. Add a document click listener as the backup.
  • Bootstrap’s documentation describes a quirk in iOS event delegation that stops a tap on empty page area from reaching the closing handler. The plugin works around it by attaching empty mouseover handlers to the body’s children while a dropdown is open.
  • A panel with a high z-index can still sit under a sibling’s stacking context. Popover elements avoid this because showPopover places them in the top layer (MDN).
  • Absolutely positioned panels spill off screen near the edges, which is the problem Floating UI’s flip and shift middleware exist to solve.

Treat the Safari and iOS items as routine cross-browser compatibility checks, not edge cases.

One wrong-fit case needs no fix. When the top-level pages fit in a single visible row, skip the dropdown. A flat list of links has no state to manage, so there is no menu to break.

How to test

  1. Keyboard only: Tab to every toggle, open with Enter and Space, close with Escape, and confirm focus lands back on the button.
  2. Screen reader pass with NVDA on Windows and VoiceOver on macOS and iOS. The APG warns of support gaps in some browser and assistive technology combinations, especially on mobile and touch devices.
  3. Touch pass on a real iPhone or iPad, where the iOS event quirk lives: tap a toggle, tap empty page area, tap a link.
  4. Automated scans: axe DevTools for ARIA and naming errors, and Lighthouse, whose SEO category reports “Links are not crawlable” for anchors without a valid href.

Scans pass or fail, but they cannot tell you whether Escape returned focus to the right button. That check stays manual.

Javascript Menus Examples To Check Out

Spotlight: Menuspy

Circles and Stuff: Radial Menu Animation

See the Pen
Radial Menu Animation
by Anton Mudrenok (@mudrenok)
on CodePen.

Slide, Slide, Slippity Slide Menu

See the Pen
Slide-out Navigation with GSAP 3
by Álvaro (@alvarotrigo)
on CodePen.

Bite into this Creative Burger

See the Pen
Creative Hamburger Menu ?
by Ahmed Hamed (@ahmedhrayyan)
on CodePen.

Cute lil’ Dropdown

See the Pen
Nice dropdown menu!
by Lukasz (@llucaso)
on CodePen.

Round & Round with Radial/Circular Menus

See the Pen
Radial / Circular Menu Concept
by Glenn McComb (@lenymo)
on CodePen.

Interactive Sidebar Fun

See the Pen
Sidebar Nav
by Álvaro (@alvarotrigo)
on CodePen.

Floating Wonder with Hover Effects

See the Pen
Fancy Navigation (+ hover effects)
by Natalia Davydova (@nat-davydova)
on CodePen.

Blossom with Blooming-Menu.js

Shutter Up and Dance!

See the Pen
Shutters
by Álvaro (@alvarotrigo)
on CodePen.

Keep it Slide-y with SuperSlide.js

Vibrant Moves Under the Line

See the Pen
Moving Underline Nav Menu
by Bennett Feely (@bennettfeely)
on CodePen.

Get Gooey with SVG

See the Pen
SVG Gooey Hover Menu Concept
by Michael Leonard (@mikel301292)
on CodePen.

Purple Reign on the Navbar

See the Pen
Purple Navbar | Nice effect for navigation
by Andrej Sharapov (@andrejsharapov)
on CodePen.

Jam with the Jelly Menu

See the Pen
Jelly Menu – Gooey effect
by Jorge (@dok)
on CodePen.

Sidebar Groove – All Sides Covered

Pull it Together with the Pull Menu

See the Pen
Pull Menu – Menu Interaction Concept
by Fabrizio Bianchi (@_fbrz)
on CodePen.

Jam on the Accordion

See the Pen
Accordion Menu
by Benjamin (@maggiben)
on CodePen.

Circle Around with Navigation

See the Pen
Circle Navigation
by Jesús Castro (@gzuzkstro)
on CodePen.

Pro in the Sidebar World

See the Pen
Pro Sidebar template
by Mohamed Azouaoui (@azouaoui-med)
on CodePen.

Keepin’ it Real with Twitter Vibes

See the Pen
Twitter Sidebar
by Aaron Iker (@aaroniker)
on CodePen.

Chatting Up with Floating Messenger

See the Pen
Floatting Draggable Menu (Messenger like)
by Andy Pagès (@andyNroses)
on CodePen.

Slide into the…Menu?

See the Pen
Sliding Menu
by Sikriti Dakua (@dev_loop)
on CodePen.

Infinity and Beyond with Scrolls

Tilt ‘n Spill with Page Effect

See the Pen
Page Tilt Effect
by Álvaro (@alvarotrigo)
on CodePen.

Ionically Side-charged

See the Pen
Ionic : Sidemenu Sample
by Justin Noel (@calendee)
on CodePen.

All Angles Covered

See the Pen
Angled Menu
by Álvaro (@alvarotrigo)
on CodePen.

Blink Once, Twice, Always

See the Pen
Staggered Blink Text Menu
by alphardex (@alphardex)
on CodePen.

Go Bold with Hover Magic

See the Pen
Bold Hover Animations with Splitting.js
by Ryan Mulligan (@hexagoncircle)
on CodePen.

Bubbling Up the Menu Scene

See the Pen
Bubble Menu
by Álvaro (@alvarotrigo)
on CodePen.

App-ish Menu for the Web

See the Pen
App Navigation
by Ian Turner (@iamturner)
on CodePen.

Getting YouTube-y with Menus

See the Pen
YouTube Menu Effect
by Romswell Roswell Parian Paucar (@romswellparian)
on CodePen.

JavaScript Menus FAQ

How do I animate a JavaScript menu?

See the Pen
Morphing Fullscreen Navigation Menu [~60fps animation]
by Álvaro (@alvarotrigo)
on CodePen.

Use CSS transitions on opacity and transform, keyed to the aria-expanded value so the animation and the announced state change together.

The hidden attribute removes the panel from rendering, so swap it for visibility rules when you want a transition to play.

Should a menu be horizontal or vertical?

See the Pen
Flat Horizontal Navigation
by Andy Tran (@andytran)
on CodePen.

Horizontal fits a top bar with a few sections, and vertical fits sidebars and deep trees.

The choice also changes keyboard mapping, since Radix’s Navigation Menu moves focus into an open panel with ArrowDown when horizontal, and with ArrowRight or ArrowLeft when vertical.

Can menu items hold icons or images?

Yes. Mega menus have room for images, according to Nielsen Norman Group (Nielsen and Li, 2017).

An icon-only button has no accessible name, so pair each icon with visible text (MDN), and mark purely decorative icons aria-hidden.

Does a sticky header need JavaScript?

See the Pen
Sticky Navigation Menu With Smooth Scrolling
by Praveen Bisht (@prvnbist)
on CodePen.

No. CSS position: sticky keeps a header in view, and a scroll listener only adds main-thread work that competes with the click handler.

The sticky navigation pattern still needs a small-viewport check, because a fixed bar takes space from content.

How do I build a right-click context menu?

See the Pen
Clicky Menus: Accessible and Progressively Enhanced Menu with Clicky Dropdowns
by Mark Root-Wiley (@mrwweb)
on CodePen.

Listen for the contextmenu event, call preventDefault, and place the panel at the pointer coordinates.

Mouse-only menus exclude keyboard users, so offer a keyboard route to the same actions, and let Escape close the panel and return focus to the element that opened it (MDN).

How deep can nested submenus go?

Two levels is a sound ceiling, and the APG disclosure navigation example stops there.

Repeat the button and list pair at each level, give every button its own aria-expanded, and close the innermost open panel first on Escape.

Nielsen Norman Group advises keeping mega menus simple, so flatten the structure before adding a third level.

Fixing Broken JavaScript Menus in the Right Order

When a menu fails several checks at once, repair the markup first. Fix the keyboard behavior next and leave the touch layout for last.

Markup leads because swapping a div for an a or button element changes what the event handlers and ARIA attributes attach to, and writing those twice wastes the work.

That means links become a elements and toggles become button elements. After that comes Escape, Tab and arrow key behavior, with focus returning to the trigger, and then the breakpoint switch and touch target sizing.

The trade-off is that the phone layout stays rough the longest, because the foundation gets fixed before the visible styling.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)