More Navigation Snippets
Nested Dropdown Menu — Free HTML CSS JS Flyout Snippet
Nested Dropdown Menu · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Nested Dropdown Menu — Recursive Multi-Level Flyout Menus

The nested dropdown is the desktop-app menu pattern where a top-level menu opens, and hovering an item with children flies a submenu out to the right — which can itself contain further submenus, several levels deep. This snippet builds it from a data tree with plain HTML, CSS, and recursive vanilla JavaScript, so the menu structure is data, not hand-written markup.
Recursive rendering from a tree
The menu is described as a nested DATA array where any item can have a children array. A build(items, level) function turns that tree into DOM: it creates a <ul>, adds a button for each item, and — if an item has children — calls itself to build the child <ul> and nests it inside. Because it's recursive, the menu supports arbitrary depth (the demo goes three levels: Share → Invite people → Can edit) with no extra code per level. Restructuring the menu is just editing the data.
Flyout submenus
The root menu drops below the button (top: 100%), while every submenu is positioned left: 100% of its parent item, so it flies out to the side like a classic cascading menu. Submenus start hidden (opacity: 0; visibility: hidden) and reveal when their parent <li> gets an .open class, transitioning opacity and a small slide. Using visibility alongside opacity ensures closed submenus aren't focusable or hoverable.
Hover logic that closes siblings
On pointerover, the handler finds the hovered item and first closes any sibling submenus at that level (so two branches are never open at once), then opens the hovered item's submenu if it has children. Closing siblings is what keeps a cascading menu tidy — moving from one parent to another collapses the old branch. The :scope > .nd-sub selector ensures it only opens the item's direct submenu, not deeper ones.
Clicks: expand vs. act
A click is interpreted by whether the item has children. Parent items only expand their submenu (handled by hover), so clicking them does nothing destructive. Clicking a leaf item (no children) is a real action, so it closes the entire menu. This mirrors how native menus behave — folders open, commands execute and dismiss.
Dismissal and accessibility
The whole menu closes on a document click outside it (the root button stops propagation so opening doesn't immediately self-close) and on the Escape key. The root carries aria-haspopup and an aria-expanded that flips with state, each submenu trigger is marked aria-haspopup, and items use role="menuitem" within role="menu" lists. Keyboard shortcuts are shown inline for leaf commands.
Customizing it
Edit the DATA tree to change the menu — add items, nest deeper, or attach icons, shortcuts, and handlers. Restyle the surfaces, adjust the flyout offset, or change the open transition. Wire each leaf's click to a real action by reading an id from the data. Pair it with a context menu or a command palette for a complete app navigation set.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Instead of tracing the recursion by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly how build(items, level) turns the nested DATA array into DOM without any level-specific code, and why the pointerover handler has to close sibling submenus before opening the hovered one to avoid two branches being open at once. The same assistant can help optimize it, for instance asking whether the recursive build() re-render on every menu open is wasteful compared to building it once and reusing the DOM, or whether the :scope selector usage could misbehave with very deep nesting. It's also useful for extending the menu: ask it to add keyboard arrow-key navigation between siblings and into submenus (not just hover), support a search filter across all levels of the tree, or add a small delay before closing a submenu so a diagonal mouse movement into it doesn't accidentally dismiss it. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "nested dropdown menu" (cascading flyout menu) in plain HTML, CSS, and JavaScript that renders recursively from a data tree — no menu library.
Requirements:
- Define the entire menu structure as a plain nested JavaScript array where any item can optionally have a children array of the same shape, supporting arbitrary depth (at least three levels deep in your example data).
- Write one recursive function that takes an items array and a nesting level and returns a fully built list element: it creates one row per item, and if that item has children, recursively calls itself to build and nest a child list inside that row — with no per-depth-level special-casing anywhere in the function.
- The top-level list must open below its trigger button; every nested list must be positioned to the right of its parent row (a flyout), starting hidden (both invisible and non-interactive) and revealing with a short opacity/transform transition when its direct parent row is marked open.
- Hovering over a row must open its child flyout (if it has one) and must close any sibling rows' flyouts at that same nesting level first, so only one branch per level is ever open simultaneously; use a scoped selector so this only affects each row's direct child list, not deeper descendants.
- Clicking a row with children must do nothing destructive (children only open via hover); clicking a row with no children (a leaf/command item) must close the entire menu tree.
- The whole menu must close when clicking anywhere outside it and when pressing Escape, and the trigger button plus every row with children must carry appropriate ARIA attributes (aria-haspopup, aria-expanded, role="menu"/"menuitem").Step by step
How to Use
- 1Paste HTML, CSS, and JSA Menu button renders, built from a nested data tree.
- 2Open the menuClick the button to drop the top-level menu.
- 3Hover a parent itemIts submenu flies out to the right.
- 4Go deeperHover a nested parent to open a further submenu.
- 5Click a leafA command item closes the whole menu.
- 6Edit the treeChange the DATA array to restructure the menu.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The menu is rendered by a recursive build(items, level) function. It creates a list, adds a button per item, and when an item has a children array it calls itself to build and nest the child list. Because it recurses, any number of levels works with no per-level code — the demo nests three deep. Changing depth is just editing the data tree.
The root menu is positioned below the button at top: 100%, while each submenu is positioned at left: 100% of its parent item with a small margin, so it appears to the right like a cascading menu. Submenus are hidden with opacity and visibility and revealed when their parent li gets an open class, with a short transition.
So only one branch is open per level, which keeps a cascading menu readable. On pointerover the handler closes sibling submenus at the hovered item's level before opening the hovered one. The :scope > .nd-sub selector ensures it opens only the item's direct submenu, not a deeper descendant.
Clicking a parent item only expands its submenu (which hover already handles), so it does nothing destructive — matching how folders work in native menus. Clicking a leaf item with no children is treated as a real command and closes the entire menu. This expand-vs-act distinction is what users expect from desktop menus.
Render the menu with a recursive component that maps the data tree, passing each node's children to a nested instance of itself. Track open submenu paths in state keyed by item id, and handle hover and click to update it. The CSS flyout positioning ports directly. In Tailwind, use group and group-hover or data-state variants to reveal submenus, with absolute left-full positioning.