Part of our complete guide: CSS Snippets →
UI Component · HTML + CSS
CSS Dropdown Menu
Three patterns — hover, click, and nested. Pure CSS for hover, native HTML for click. All copy-paste ready.
Last updated: October 2026 · 7 min read
A dropdown menu opens a list of options when you hover or click a trigger. Pure CSS works for hover. For click behavior, use the native <details> element or add 5 lines of JavaScript.
What you get: Three complete patterns — hover-only (CSS only), click-based (native HTML), and nested (multi-level). Plus the pitfalls that break each.
Why dropdown menus are tricky
Dropdowns look simple but fail in predictable ways. The menu disappears when you move your mouse toward it because there's a gap between the trigger and menu — you hover off before reaching the target. On mobile, hover doesn't exist, so the dropdown never opens. And when you click a link, the menu stays open because nothing told it to close.
The three patterns below solve these problems differently. The hover pattern is the smallest — 30 lines of CSS, no JavaScript. It works perfectly on desktop, fails completely on touch devices. The click pattern uses HTML's native <details> element — the browser handles open/close for you, and it works everywhere. The nested pattern handles submenus using pure CSS positioning.
Pick based on your use case. Navigation menus on desktop-first sites: hover. Settings menus, mobile-first: click. Multi-level mega-menus: nested (with JS for the outer toggle).
🎬 Live Demos
Hover or click each trigger. Copy the code that works for your case.
🖱️ Hover Dropdown
<div class="dd-hover">
<button class="dd-hover-trigger">Hover Me ▾</button>
<div class="dd-hover-menu">
<a href="#">Profile</a>
<a href="#">Settings</a>
<a href="#">Logout</a>
</div>
</div>
<style>
.dd-hover {
position: relative; display: inline-block;
}
.dd-hover-trigger {
background: #3b82f6; color: #fff;
padding: 10px 20px; border-radius: 8px;
border: none; font-weight: 600;
cursor: pointer; font-family: inherit;
}
.dd-hover-menu {
position: absolute; top: 100%; left: 0;
min-width: 180px;
background: #fff; border: 1px solid #e2e8f0;
border-radius: 8px; padding: 6px 0;
margin-top: 4px;
opacity: 0; visibility: hidden;
transform: translateY(-4px);
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dd-hover:hover .dd-hover-menu {
opacity: 1; visibility: visible;
transform: translateY(0);
}
.dd-hover-menu a {
display: block;
padding: 8px 16px;
color: #1e293b;
text-decoration: none;
font-size: 14px;
}
.dd-hover-menu a:hover {
background: #f1f5f9;
}
</style>
👆 Click Dropdown
Click Me
<details class="dd-click">
<summary>Click Me</summary>
<div class="dd-click-menu">
<a href="#">Profile</a>
<a href="#">Settings</a>
<a href="#">Logout</a>
</div>
</details>
<style>
.dd-click {
position: relative; display: inline-block;
}
.dd-click summary {
background: #3b82f6; color: #fff;
padding: 10px 20px; border-radius: 8px;
font-weight: 600; cursor: pointer;
list-style: none; user-select: none;
}
.dd-click summary::-webkit-details-marker {
display: none;
}
.dd-click summary::after {
content: ' ▾';
}
.dd-click[open] summary::after {
content: ' ▴';
}
.dd-click-menu {
position: absolute; top: 100%; left: 0;
min-width: 180px;
background: #fff; border: 1px solid #e2e8f0;
border-radius: 8px; padding: 6px 0;
margin-top: 4px;
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dd-click-menu a {
display: block;
padding: 8px 16px;
color: #1e293b;
text-decoration: none;
font-size: 14px;
}
.dd-click-menu a:hover {
background: #f1f5f9;
}
</style>
📂 Nested Dropdown
<div class="dd-nested">
<button class="dd-nested-trigger">Categories ▾</button>
<ul class="dd-nested-menu">
<li><a href="#">Web Development</a></li>
<li>
<a href="#">Design ▸</a>
<ul class="dd-submenu">
<li><a href="#">Figma</a></li>
<li><a href="#">Photoshop</a></li>
<li><a href="#">Illustrator</a></li>
</ul>
</li>
<li><a href="#">Marketing</a></li>
</ul>
</div>
<style>
.dd-nested {
position: relative; display: inline-block;
}
.dd-nested-trigger {
background: #3b82f6; color: #fff;
padding: 10px 20px; border-radius: 8px;
border: none; font-weight: 600;
cursor: pointer; font-family: inherit;
}
.dd-nested-menu {
position: absolute; top: 100%; left: 0;
min-width: 200px;
background: #fff; border: 1px solid #e2e8f0;
border-radius: 8px; padding: 6px 0;
margin: 4px 0 0; list-style: none;
opacity: 0; visibility: hidden;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dd-nested:hover .dd-nested-menu {
opacity: 1; visibility: visible;
}
.dd-nested-menu > li {
position: relative;
}
.dd-nested-menu > li > a {
display: block;
padding: 8px 16px;
color: #1e293b;
text-decoration: none;
font-size: 14px;
}
.dd-nested-menu > li:hover > a {
background: #f1f5f9;
}
.dd-submenu {
position: absolute; top: 0; left: 100%;
min-width: 160px;
background: #fff; border: 1px solid #e2e8f0;
border-radius: 8px; padding: 6px 0;
margin-left: 4px; list-style: none;
opacity: 0; visibility: hidden;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dd-nested-menu > li:hover > .dd-submenu {
opacity: 1; visibility: visible;
}
.dd-submenu a {
display: block;
padding: 8px 16px;
color: #1e293b;
text-decoration: none;
font-size: 14px;
}
.dd-submenu a:hover {
background: #f1f5f9;
}
</style>
How each pattern works
The wrapper .dd-hover has position: relative and the menu has position: absolute. The menu hides by default (opacity: 0, visibility: hidden). When you hover the wrapper, the :hover pseudo-class makes the menu visible. This is the entire mechanism — 20 lines of CSS, no event listeners.
The <details> element opens and closes natively when you click its <summary>. No JavaScript needed. The [open] attribute controls visibility, which we hide with list-style: none on the summary and style the rest of the menu with position: absolute. Works everywhere — including mobile — because clicks work on every device.
Each <li> in the main menu is position: relative. The submenu inside it is position: absolute; left: 100%, which places it to the right of the parent item. Hovering the parent <li> makes the submenu visible. The whole thing nests without JavaScript, though it can clip on narrow screens.
🔬 Edge cases
The problems that break dropdowns in production.
The menu has margin-top: 4px — a gap between trigger and menu. Moving your mouse toward the menu drops the hover. Fix: use padding-top on the menu instead of margin-top, or add a transparent pseudo-element bridging the gap.
If any ancestor has overflow: hidden, the dropdown gets clipped. Common in card layouts. Fix: either remove overflow from the ancestor, or use position: fixed for the menu (needs JS to compute position).
If the dropdown appears under other content, check for a parent with transform or filter — those create a new stacking context. Fix: add position: relative; z-index: 1 to the dropdown wrapper, or increase its z-index.
Touch devices don't have a hover state. The first tap may trigger hover styles, but they often get stuck on. Fix: use the <details> pattern for anything that needs to work on mobile.
@media (hover: none) {
/* Replace hover with :active or use the click pattern */
.dd-hover:hover .dd-hover-menu { opacity: 0; visibility: hidden; }
}
Pure CSS can't detect outside clicks. The <details> pattern closes on Escape and on clicking the summary, but not on clicking elsewhere. Fix with 5 lines of JS:
document.addEventListener('click', e => {
document.querySelectorAll('details.dd-click[open]').forEach(d => {
if (!d.contains(e.target)) d.removeAttribute('open');
});
});
Keyboard users tab to the trigger but the menu never opens. Fix: add :focus-within to the trigger selector:
.dd-hover:hover .dd-hover-menu,
.dd-hover:focus-within .dd-hover-menu {
opacity: 1;
visibility: visible;
}
In the nested pattern, if the parent menu closes while the submenu is open, both stay in memory. Fix: the CSS already handles this because visibility: hidden propagates to children. No JS needed.
Some users can't tolerate the fade-in animation. Respect it:
@media (prefers-reduced-motion: reduce) {
.dd-hover-menu, .dd-nested-menu, .dd-submenu {
transition: none;
}
}
♿ Accessibility
Pure CSS dropdowns are famous for failing screen readers and keyboard users. Here's the minimum to fix each pattern.
Hover-only dropdowns need :focus-within to open on keyboard tab. Add this to the trigger CSS:
.dd-hover:hover .dd-hover-menu,
.dd-hover:focus-within .dd-hover-menu {
opacity: 1;
visibility: visible;
}
The trigger should be a <button>, not a <div>, so it's tabbable by default. The menu should have role="menu" and each item role="menuitem".
<details> and <summary> give you keyboard navigation, screen reader announcements, and Escape-to-close for free. It's the most accessible dropdown pattern available without JavaScript. Use it for anything user-facing.
⚡ Performance
| Pattern | CSS weight | JS required |
|---|---|---|
| Hover | ~30 lines | None |
| Click (<details>) | ~35 lines | Optional (outside-click close) |
| Nested | ~60 lines | None |
Total weight: All three patterns combined are under 2KB of CSS. That's smaller than most icon fonts. There's no reason to load a JS dropdown library for this.
⚠️ Common mistakes
- Using
display: none/display: blockfor the animation. This skips the transition. Useopacity+visibilityinstead. - Forgetting
position: relativeon the wrapper. The menu positions against the page, not the trigger. - Using
<div>for the trigger. Not keyboard-accessible. Use<button>or<summary>. - Missing
:focus-withinsupport. Keyboard users can never open the menu. - Menu z-index too low. Menu appears under nearby content. Add
z-index: 10or higher. - No outside-click close. The menu stays open forever. Add the 5-line JS snippet from above.
- Hover on touch devices. Works once, then sticks. Use
@media (hover: none)to disable or switch to click pattern. - Anchors with
href="#"doing nothing. Real links or real buttons — never fake anchors.
❓ Frequently asked questions
Can I make a dropdown menu with pure CSS?
Yes for hover-triggered dropdowns. Pure CSS can't handle click-to-open or keyboard navigation. For click behavior, you need a small amount of JavaScript or a native HTML <details> element.
Why does my dropdown disappear when I move the mouse to it?
There's a gap between the trigger and the menu. Fix: add padding-bottom to the parent element, or a transparent pseudo-element bridging the gap. Both keep the dropdown open while you move toward it.
How do I make a dropdown accessible with keyboard?
Use a button element, manage aria-expanded, and handle focus states. Pure CSS hover is not accessible alone. Add :focus-within to trigger on keyboard tab, and manage focus with JavaScript for full support.
What is the difference between hover and click dropdowns?
Hover dropdowns open when the cursor enters the trigger. Click dropdowns require an explicit action. Hover fails on touch devices (no cursor). Click works everywhere but needs JavaScript or :target/:checked hacks for pure CSS.
How do I close the dropdown when clicking outside?
Pure CSS can't detect outside clicks. You need JavaScript: document.addEventListener('click', e => { if (!menu.contains(e.target)) close(); }). This is the standard pattern.
Can I nest dropdowns inside dropdowns?
Yes. Nested dropdowns work with CSS if you position the child menu to the right of its parent item and use :hover on the parent item. They're fragile on mobile though — consider a different pattern there.
Do dropdown menus hurt accessibility?
They can if built poorly. Best practice: use <button> for triggers, aria-haspopup='true', aria-expanded state, keyboard support for arrow keys and Escape, and focus management. Pure CSS solutions sacrifice some of this.
What's the native HTML alternative to a custom dropdown?
The <details> and <summary> elements create a native expandable dropdown with zero CSS or JS. It's accessible by default and works without any scripting. Modern browsers support it everywhere.