Right, nested menus. This is where we go from “handy navigation” to “full-on application architecture.” Done well, it’s a thing of beauty and usability. Done poorly, and you’ve built a Russian nesting doll of user frustration. My goal is to make sure you’re in the first camp.

The core idea is simple: a parent menu entry doesn’t link to a page itself, but instead, when you hover or click it, it reveals a child menu—a sub-list of more specific options. It’s the “just one more level” of web navigation. We use this for a reason: it helps users mentally categorize information without overwhelming them with fifty top-level links. Think of it as the difference between a messy desk and one with a few well-labeled drawers.

The HTML: It’s All About the Hierarchy

Forget fancy JavaScript for a moment. This structure lives or dies by your HTML. You need proper semantic nesting. The child menu isn’t a sibling to the parent; it’s a child of the parent list item. This isn’t just philosophical—it’s critical for CSS and accessibility.

Here’s the canonical structure. Notice how the <ul> for the child menu is inside the parent <li>. This is the most important thing to get right.

<nav class="main-nav">
    <ul>
        <li><a href="/">Home</a></li>
        <li>
            <a href="/products">Products</a> <!-- The Parent -->
            <ul class="sub-menu"> <!-- The Child container -->
                <li><a href="/products/widgets">Widgets</a></li>
                <li><a href="/products/gadgets">Gadgets</a></li>
                <li><a href="/products/thingamajigs">Thingamajigs</a></li>
            </ul>
        </li>
        <li><a href="/about">About Us</a></li>
        <li><a href="/contact">Contact</a></li>
    </ul>
</nav>

The Core CSS: Hiding and Showing

The magic is in the CSS. We hide the child menu by default and then reveal it when the user interacts with the parent. This is almost universally done with the :hover pseudo-class. It’s simple but has a critical flaw we’ll address in a minute.

.main-nav ul { /* Targets ALL ul's in the nav */
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex; /* Makes the top-level horizontal */
}

.main-nav li {
    position: relative; /* This is crucial! The child menu will position itself relative to this li */
}

.sub-menu {
    display: none; /* Hide the child menus by default */
    position: absolute; /* Yank them out of the document flow */
    top: 100%; /* Position it right at the bottom of the parent li */
    left: 0;
    background-color: #fff;
    box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Not required, but let's be classy */
    min-width: 150px; /* Prevents it from collapsing to the width of the longest word */
}

.main-nav li:hover > .sub-menu { /* Show the sub-menu when its direct parent li is hovered */
    display: block;
}

The :hover Problem and the JavaScript Fix

Here’s the rough edge: the :hover approach is terrible for anyone not using a precise pointing device. Try hitting that menu on a touchscreen. You can’t. It’s a classic example of the web forgetting that not everyone is on a desktop. The designers of the CSS spec clearly didn’t foresee the mobile revolution, and now we have to clean up the mess.

The solution is to use JavaScript to add an active class on both hover and click (or touch), making it universally accessible.

// Get all menu items that have a child sub-menu
const menuItems = document.querySelectorAll('.main-nav li:has(.sub-menu)');

menuItems.forEach(item => {
    // Add a toggle on click for touch devices
    item.addEventListener('click', function(e) {
        // If the click is directly on the link, prevent it from actually navigating
        if (e.target.tagName === 'A') {
            e.preventDefault();
        }
        this.classList.toggle('is-active');
    });

    // Add the hover effect for mouse users for better UX
    item.addEventListener('mouseenter', function() {
        this.classList.add('is-active');
    });
    item.addEventListener('mouseleave', function() {
        this.classList.remove('is-active');
    });
});

Now, update your CSS to target the class instead of the pseudo-class:

.main-nav li.is-active > .sub-menu {
    display: block;
}

The Accessibility Tax (It’s Worth Paying)

This is non-negotiable. Nested menus are a minefield for screen readers and keyboard users. You must make them accessible. This means:

  1. aria-haspopup="true" on the parent <a> tells assistive tech there’s a popup.
  2. aria-expanded="false" on that same link, toggling to "true" when the menu is open. Our JS can handle this.
  3. Keyboard Navigation: You need to trap focus within the open menu and allow navigation with arrow keys. This is a whole other tutorial, but know that just using a <ul> is a good start because screen readers understand lists.

The updated HTML for the parent link becomes:

<a href="/products" aria-haspopup="true" aria-expanded="false">Products</a>

The “Don’t Be a Jerk” Best Practices

  1. Don’t Nest Too Deep: Two levels are usually fine. Three is pushing it. Four is a cry for help. Users get lost.
  2. Visual Cues: Use a small arrow (▾) or similar icon next to parent items. It’s a universal signal that says “there’s more here.”
  3. Don’t Make Them Too Small: The hover area for revealing the child menu is the entire <li>. Make sure it’s adequately sized. Nothing is worse than a menu that disappears because your mouse moved a pixel.
  4. Close on Mouse-Out: Our basic JS does this. The menu should disappear when the user’s intent (hover or click) is clearly elsewhere.

The key takeaway? The HTML structure is your foundation. CSS makes it pretty. JavaScript makes it work for everyone. And accessibility makes it not a liability. Build in that order.