Episode 4 of 8

CSS Only Dropdown Menu

Build a fully functional, multi-level dropdown navigation menu using only CSS — hover-triggered with smooth transitions.

CSS Only Dropdown Menu

Dropdown menus are a staple of web navigation. In this episode you will build a fully functional, multi-level dropdown menu using only CSS. The menu will appear on hover with smooth transitions and support nested submenus.

The HTML Structure

<nav class="navbar">
    <ul class="nav-menu">
        <li class="nav-item">
            <a href="#">Home</a>
        </li>
        <li class="nav-item has-dropdown">
            <a href="#">Products</a>
            <ul class="dropdown">
                <li><a href="#">Electronics</a></li>
                <li><a href="#">Clothing</a></li>
                <li><a href="#">Books</a></li>
            </ul>
        </li>
        <li class="nav-item has-dropdown">
            <a href="#">Services</a>
            <ul class="dropdown">
                <li><a href="#">Web Design</a></li>
                <li><a href="#">SEO</a></li>
                <li class="has-dropdown">
                    <a href="#">Marketing &raquo;</a>
                    <ul class="dropdown sub-dropdown">
                        <li><a href="#">Social Media</a></li>
                        <li><a href="#">Email</a></li>
                        <li><a href="#">PPC</a></li>
                    </ul>
                </li>
            </ul>
        </li>
        <li class="nav-item">
            <a href="#">Contact</a>
        </li>
    </ul>
</nav>

The structure uses nested <ul> elements. Each dropdown is a <ul class="dropdown"> inside a parent <li>. This nesting supports multi-level dropdowns naturally.

Base Navigation Styles

.navbar {
    background: #2c3e50;
    padding: 0 20px;
}

.nav-menu {
    list-style: none;
    display: flex;
    margin: 0;
    padding: 0;
}

.nav-item {
    position: relative;
}

.nav-menu a {
    display: block;
    padding: 16px 20px;
    color: #ecf0f1;
    text-decoration: none;
    font-size: 15px;
    transition: background 0.3s ease, color 0.3s ease;
}

.nav-menu a:hover {
    background: #34495e;
    color: #3498db;
}

The top-level menu is a horizontal flex container. Each nav item uses position: relative so that its dropdown (which will be absolutely positioned) aligns correctly below it.

The Dropdown — Hidden by Default

.dropdown {
    list-style: none;
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 200px;
    background: #34495e;
    padding: 0;
    margin: 0;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px);
    transition: opacity 0.3s ease,
                transform 0.3s ease,
                visibility 0.3s ease;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
    border-radius: 0 0 6px 6px;
    z-index: 1000;
}

.dropdown a {
    padding: 12px 20px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.dropdown li:last-child a {
    border-bottom: none;
}

The dropdown is absolutely positioned below its parent item (top: 100%). It is hidden using a combination of opacity: 0, visibility: hidden, and translateY(-10px). We use visibility instead of display: none because visibility can be transitioned but display cannot.

Showing the Dropdown on Hover

.has-dropdown:hover > .dropdown {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

The child combinator (>) ensures that only the direct dropdown child is shown, not nested sub-dropdowns. On hover, the dropdown fades in and slides down from its shifted position.

Nested Sub-Dropdowns

.sub-dropdown {
    top: 0;
    left: 100%;
    border-radius: 0 6px 6px 6px;
}

/* Indicator arrow for items with sub-dropdowns */
.dropdown .has-dropdown > a::after {
    content: "\203A";
    float: right;
    margin-left: 10px;
    font-size: 18px;
}

Sub-dropdowns position themselves to the right of their parent item (left: 100%) and start from the top (top: 0). The ::after pseudo-element adds a right-pointing arrow to indicate further nesting.

Adding a Dropdown Indicator Arrow

.nav-item.has-dropdown > a::after {
    content: "\25BE";
    margin-left: 6px;
    font-size: 12px;
    opacity: 0.7;
}

.has-dropdown:hover > a::after {
    opacity: 1;
}

A small downward-pointing triangle (\25BE) is added after the text of any top-level item that has a dropdown, providing a visual hint to the user.

Preventing Gap Issues

/* Invisible bridge to prevent dropdown from closing */
.has-dropdown::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    height: 10px;
}

If there is a small gap between the nav item and the dropdown, moving the mouse from the item to the dropdown causes the hover state to break. This invisible bridge pseudo-element fills that gap and keeps the dropdown open during the mouse movement.

Key Takeaways

  • Use opacity + visibility + transform instead of display: none for animated show/hide — display cannot be transitioned
  • The child combinator (>) in .has-dropdown:hover > .dropdown prevents nested dropdowns from all opening at once
  • Nested <ul> elements make multi-level dropdowns possible with pure CSS
  • Position sub-dropdowns with left: 100% and top: 0 to slide them out to the right
  • An invisible bridge pseudo-element prevents the dropdown from closing when the mouse moves across a gap
  • Unicode characters like \25BE (down triangle) and \203A (right angle) serve as lightweight dropdown indicators