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 »</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+transforminstead ofdisplay: nonefor animated show/hide —displaycannot be transitioned - The child combinator (
>) in.has-dropdown:hover > .dropdownprevents nested dropdowns from all opening at once - Nested
<ul>elements make multi-level dropdowns possible with pure CSS - Position sub-dropdowns with
left: 100%andtop: 0to 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