Top 5 CSS Navigation Menu Mistakes

Zoran JamborZoran Jambor
10 min read

Find out how to improve the UI of your navigation menus with a few lines of CSS. Along the way, you’ll learn how to create a delayed closing effect for the dropdown menu, utilize the :has() pseudo-class to simplify your HTML structure, and more.

This article is a result of my subjective experience of noticing problems with the navigation menus of products and apps I’ve been using in the past few weeks. The same patterns and problems are repeated often, and I outline them here.

💡
This guide is focused on the UI of the navigation menus for the sake of brevity and clarity. One of the most important aspects of any interactive component, including navigation menus, is accessibility, and I will cover this in a follow-up guide.

Demo & Setup

The navigation Menu HTML structure is straightforward. The top-level navigation menu is designated with the class .menu and dropdown with .submenu. I’ve applied some basic styling—you’ll find the relevant demo code in the @layer demo { ... } at the top of CSS.

The final demo, with all tips from this article applied:

The HTML here is cleaned up for the sake of brevity. I would heavily rely on classes instead of cascade in a real-world project to reduce specificity.

The dropdown is styled horizontally to make the mistakes more apparent in this simple demo—I'm not suggesting that the horizontal pattern is better or preferable to a classic, vertical one.

Mistake #1: Small target areas

The target area of text-only menus is often too small, making the interactions with navigation items cumbersome, especially on touch devices.

The target area of anchor links covers only text itself, requiring more precision and focus from users.

The way to fix this mistake is to add additional padding around the anchor links, thus expanding the interactive, clickable area.

/**
 * Expand the target area 
 * for links by adding padding. 
 */
.menu a {
    padding-inline: 1rem;
    padding-block: 0.5rem;
}

Once you apply additional padding, the perceived gap between items will increase, so make sure to adjust it, keeping in mind that there always should be a space between menu items.

After adding padding to anchor links, the target area is expanded (yellow highlight) and much more forgiving.

Mistake #2: Not using gap with Flexbox

Navigation menus are most often created using Flexbox, as it provides an easy way to adjust positioning and alignment, but the Flexbox is not utilized fully, and the spacing between items is added using margin.

Patterns I see in this case, which all work but are unnecessary:

/* Using an owl selector to add margin only on subsequent items. */
.menu > * + * { margin-left: 1rem; }

/* Using :not() to avoid adding margin to the last (or first) item. */
.menu > li:not(:last-child) { margin-right: 1rem; }

/* Resetting the margin on last/first item to zero. */
.menu > li { margin-right: 1rem; }
.menu > li:last-child { margin-right: 0; }

Instead, you can use the gap property with Flexbox to add spacing only between items without.

/** 
 * You can use the `gap` property 
 * with Flexbox. 
 */
.menu {
    display: flex;
    gap: 1rem;
}

The gap property for Flexbox will add spaces only between Flexbox items.

Note that the gap property did not work with Flexbox initially when Flexbox was first released, so you had to resort to a different solution, but this is now in the Baseline and has worked in all modern browsers since April 2021.

Mistake #3: Unforgiving target areas for dropdowns

Similar to the first point, navigation menus with dropdowns should have forgiving target areas, and this specifically means that you need to adjust the target area to the path most likely taken by a user.

The yellow arrow is showing a path over empty space the user might take when targeting the first item in the dropdown—thus unintentionally closing the dropdown.

The easiest way to fix this mistake is to add a pseudo-element and position it to cover the empty area users will likely utilize.

/**
 * Expand the target area of dropdown menus
 */
.submenu::after {
    content: "";
    display: block;
    position: absolute;
    width: 100%;
    height: 3rem;
    bottom: 100%;
    left: 0;
    /* background: darkred; */
}

To test this and tweak the positioning, add a background to the pseudo-element, and once you’re happy with the result, remove it, making the element transparent. This will increase the target area without affecting the visual style.

An additional pseudo-element (dark red) that expands the target area of the dropdown. In production, you’d make it transparent.

Note that the element is going underneath other top-level menu items, so it won’t interfere with user interactions with those elements.

Mistake #4: No delay before closing dropdowns

This point is tied directly with the previous one—you want to create forgiving target areas for clicks, touch, and hover events.

If a user accidentally moves the mouse away from the dropdown menu, you don’t want to close it immediately; you want to give the user a chance to reposition their pointer and continue where they left off without starting from zero by moving the mouse to the top-level item. As you suspect, this becomes more and more crucial as the number of your dropdown menu levels increases.

You can achieve this with a bit of JavaScript. When the user hovers over an item with a dropdown, you add a class to the item or tweak the style directly, and after the user hovers away from the dropdown tree, you delay the closing of the dropdown a bit.

/**
 * A simplified example of how you might
 * implement menu delay with one sublevel.
 */
const menus = document.querySelectorAll(".menu > li");
const submenus = document.querySelectorAll(".submenu");

// Track the timeout delay
let hideTimeout = null;

// Add event listeners to all top-level items
menus.forEach((menu) => {
    const submenu = menu.querySelector(".submenu");

    menu.addEventListener("mouseenter", (e) => {

        // Immediately hide all (other) dropdown menus
        submenus.forEach((otherSubmenu) => {
            otherSubmenu.style.display = "";
        });

        // And show the child submenu
        if (submenu) {
            clearTimeout(hideTimeout);
            submenu.style.display = "flex";
        }
    });

    // There's no need for mouseleave event if the item has no dropdown
    if (!submenu) return;

    // Close the menu after the timeout 
    // This can be canceled in the mouseenter event
    menu.addEventListener("mouseleave", () => {
        hideTimeout = setTimeout(() => {
            submenu.style.display = "";
        }, 500);
    });
});

This is a simplified example of how you might handle a delayed closing—it doesn’t take into consideration that there might be additional sublevels of dropdown menus, nor the positioning of the dropdown when there isn’t enough space on the screen.

CSS-Only Dropdown Delay

Alternatively, you can achieve a delayed closing effect without JavaScript, using only modern CSS.

💡
This is an experimental technique that aims to showcase the power of modern CSS, but it still doesn't work in all browsers and currently has a major downside in Chrome—don't use it in production.

The dropdown menu show/hide interaction is created using the display property:

/* Hide the dropdown by default. */
.submenu {
    display: none;
    /* Plus properties related to positioning & styling. */
}

/* Show the dropdown when its parent menu item is hovered. */
.menu li:hover a + .submenu {
    display: flex;
}

Using transition-behavior, you can transition the display property and have it delay closing, mimicking the complex JavaScript behavior in a few lines of CSS.

/**
 * Delay the closing of the dropdown 
 * menu on mouseout using only CSS.
 */
.submenu {
    transition: display 1s;
    transition-behavior: allow-discrete;
}

You can transition non-animatable properties using transition-behavior: allow-discrete; as there is no in-between state from none to block (or flex), the value will be flipped on transition-in before the start of the animation and on transition-out after the end of the animation, as defined in transition-duration, meaning that the dropdown will stay entirely visible until the transition finishes (1s in the example).

Unfortunately, this isn’t a viable option at the moment, as it doesn’t work in Firefox (Firefox doesn’t support transitioning display property yet), and it is severely bugged in Chrome, where the items in the dropdown become inaccessible when the transition-delay is triggered. This means that Safari is currently the only browser where this works as expected.

Additionally, we need to create the code to close the dropdown immediately when a user hovers over another top-level item. An approach I’ve been exploring is using :has() pseudo-class to set the transition to none (thus canceling the transition and changing the value immediately) when a top-level item is hovered over.

/**
 * Stop the transition when another menu item is hovered
 * by resetting the transition to `none` 
 * on all menu items that are not hovered over, 
 * but only when one of the menu items is in the state of hover 
 */
.menu ul:has(> .li:hover) .li:not(:hover) .submenu {
    transition-property: none;
}

Unfortunately, this isn't a reliable approach as it doesn't work in Safari—it seems that the transition can't be interrupted once it starts.

A demo showing how CSS-only dropdown menu delay works, along with issues in various browsers.

This CSS-only solution will hopefully be supported cross-browser in the near future, but until then, you still need a bit of JavaScript to create a close delay effect for your dropdown menus.

How to use :has() pseudo-class to add arrows on menu items with submenus

You can utilize :has() to easily create an arrow that indicates an existing dropdown without adding additional classes to HTML.

/**
 * Add an arrow to menu items that have a submenu, 
 * defined as `a` elements that have the next sibling `.submenu`
 */
.menu a:has(+ .submenu)::after {
    content: "▼";
    margin-left: 1em;
    font-size: 0.5em;
    position: relative;
    top: -0.2em;
}

An arrow indicating a presence of a dropdown menu created using :has() pseudo-class.

The last of the common mistakes I usually see is using in-page anchor links without animating them.

By default, when you create an anchor link and tie it to a section on the page using an id attribute, the page will immediately jump to the target section once the user clicks it.

A much nicer and clearer experience is to scroll the page elegantly to the target section. You don’t even have to use JavaScript to handle this; there is a CSS property, scroll-behavior, that lets you define how the browser handles the scrolling when triggered by the navigation or CSSOM scrolling APIs.

You can set it to any element with a scrollable overflow; this would be the root element for the viewport (entire page).

/**
 * Enable smooth scroll for anchor links.
 */
html {
    scroll-behavior: smooth;
}

If you are using scroll-behavior: smooth;, you’ll want to make sure to provide an alternative experience to users who prefer an interface that removes, reduces, or replaces motion-based animations.

/**
 * Disable anchor link animations for 
 * users who prefer less motion.
 */
@media (prefers-reduced-motion) {
    html {
        scroll-behavior: auto;
    }
}

Proving my point, Hashnode, the platform I’m using to publish this, should add scroll-behavior: smooth; to improve the experience for the Table of contents element linking to the sections of this article.

Conclusion

Most of the advice shared here is straightforward and obvious, but I still see many of these mistakes in the wild, so take this as a reminder that with just a few lines of CSS, you can make your navigation menus and UI much more user-friendly and usable.

As I’ve already mentioned, this is just a part of the story of creating good navigation menus. The other significant part that you shouldn’t ignore is accessibility, and I will cover this in a follow-up article where I’ll show you how to make dropdown menus accessible.

Further Learning

0
Subscribe to my newsletter

Read articles from Zoran Jambor directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Zoran Jambor
Zoran Jambor

Frontend developer & content creator, author of CSS Weekly.