You know what? I’ve built a lot of menus. Some cute. Some messy. The one that always gets folks is the dropdown that sits on top of the page, not pushing stuff down. I’ve broken layouts, fought z-index gremlins, and even made a menu vanish under a slider on a Friday night. Fun times. But if you want the step-by-step blow-by-blow of my most stubborn overlay bug, I wrote it up in this detailed post.
Here’s what worked for me, with real code I ship.
Quick outline
- A simple overlay dropdown that just works
- Keeping the menu above everything else (z-index and friends)
- Click vs hover, plus a tiny script
- A bigger “mega” menu that doesn’t shove the page
- Gotchas I hit on real sites
- How this felt with Tailwind and Bootstrap
For a quick way to prototype these patterns before wiring them into your own site, you can try the playground at CSS Menu Tools, which lets you fiddle with dropdowns, z-index values, and shadows in real time.
The simple one: absolute + z-index
My first fix was basic. I set the parent to relative. I set the dropdown to absolute. Then I gave it a z-index so it sits on top like the top card in a stack. Simple stuff, like stacking paper. If you’d like a deeper primer on how position and stacking contexts actually work under the hood, DigitalOcean’s tutorial on layout features, position, and z-index is a great refresher.
Here’s the exact code I used on my snack shop header:
<nav class="nav">
<button class="menu-btn" aria-haspopup="true" aria-expanded="false" id="products-btn">
Products
</button>
<ul class="dropdown" aria-labelledby="products-btn">
<li><a href="#">Chips</a></li>
<li><a href="#">Candy</a></li>
<li><a href="#">Drinks</a></li>
</ul>
</nav>
.nav {
position: relative; /* anchor for the dropdown */
display: inline-block;
}
.menu-btn {
background: #111;
color: #fff;
border: 0;
padding: 8px 12px;
cursor: pointer;
}
.dropdown {
position: absolute;
top: 100%; /* sits right under the button */
left: 0;
z-index: 1000; /* high enough to float above content */
display: none;
min-width: 180px;
background: #fff;
border: 1px solid #ddd;
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
list-style: none;
margin: 6px 0 0;
padding: 6px 0;
}
.dropdown a {
display: block;
padding: 8px 12px;
color: #222;
text-decoration: none;
}
.dropdown a:hover {
background: #f5f5f5;
}
.nav.open .dropdown {
display: block;
}
And a tiny script to open and close it:
const nav = document.querySelector('.nav');
const btn = document.querySelector('.menu-btn');
btn.addEventListener('click', () => {
const isOpen = nav.classList.toggle('open');
btn.setAttribute('aria-expanded', String(isOpen));
});
document.addEventListener('click', (e) => {
if (!nav.contains(e.target)) {
nav.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
}
});
This one felt clean. Fast to ship. It overlays the page, so the content below doesn’t jump. No layout shift. No drama.
The layer cake: keeping it above everything
Then I hit a snag on my food blog. The dropdown hid behind a fancy image slider. I wanted to cry a little. Here’s the thing: z-index works only if the parent allows it. If the parent makes a new “layer world” (like when it has transform or a set z-index), your menu can get trapped below. That exact pain point is dissected in a popular StackOverflow discussion about forcing a dropdown div to the correct z-index, and it saved me a ton of head-scratching.
My quick checks:
- The dropdown has position: absolute or fixed.
- The parent has position: relative (but no random z-index).
- No overflow: hidden on a wrapper that needs to show the menu.
- If a parent uses transform, move the menu out or remove that transform.
A simple fix that saved me:
/* Bad: this can trap the dropdown underneath */
.header {
transform: translateZ(0); /* I removed this on the site with the bug */
}
/* Good: let it layer naturally, or manage z-index carefully */
.header {
position: relative;
z-index: 10; /* header on top */
}
.dropdown {
z-index: 30; /* dropdown above header and content */
}
Think of z-index like jersey numbers. Higher number gets seen first.
Click, hover, and phones
Hover works on desktop. But my nephew tried my menu on an iPad, and nothing opened. Oops. So I switched to click for mobile and kept hover as a nice extra.
CSS-only hover (fine for desktop):
.nav:hover .dropdown {
display: block;
}
But keep the click script from above for phones. I also add keyboard love:
btn.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
nav.classList.add('open');
btn.setAttribute('aria-expanded', 'true');
const firstItem = nav.querySelector('.dropdown a');
firstItem && firstItem.focus();
}
});
It’s not fancy. It’s friendly.
The bigger one: a “mega” menu that overlays clean
I built a wide menu for a shop. Big columns. Pictures. No pushing the page down. Here’s a trimmed version I used:
<header class="site-header">
<nav class="menu">
<div class="item has-mega">
<button class="menu-btn" aria-expanded="false">Shop</button>
<div class="mega">
<section>
<h3>Women</h3>
<a href="#">Tops</a>
<a href="#">Jeans</a>
</section>
<section>
<h3>Men</h3>
<a href="#">Tees</a>
<a href="#">Sneakers</a>
</section>
<aside class="promo">Summer sale 30% off</aside>
</div>
</div>
</nav>
</header>
.site-header {
position: sticky;
top: 0;
background: #fff;
z-index: 20; /* header above content */
border-bottom: 1px solid #eee;
}
.item {
position: relative; /* anchor */
}
.mega {
position: absolute;
left: 0;
top: 100%;
z-index: 40; /* above header and page */
display: none;
width: min(100vw, 900px);
background: #fff;
border: 1px solid #ddd;
box-shadow: 0 12px 30px rgba(0,0,0,0.18);
padding: 16px;
gap: 24px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.item.open .mega { display: grid; }
.mega h3 { margin: 0 0 8px; font-size: 14px; color: #444; }
.mega a { display: block; padding: 6px 0; color: #222; text-decoration: none; }
.mega a:hover { text-decoration: underline; }
.promo { background: #f9f9ff; padding: 12px; border-radius: 6px; }
Same JS toggle pattern from the small menu. It sat on top of the hero image and the slider, smooth as butter.
Common gotchas I hit (and fixed)
-
The dropdown hides behind a slider or video
- Raise z-index on the dropdown and the header. Remove transforms on parents if you can.
-
The dropdown gets clipped
- A parent has overflow: hidden. Change it to overflow: visible on the wrapper that needs to show the menu.
-
It jumps around on scroll
- For menus that must stick to the viewport, use position: fixed and set left/top from the button’s box. I’ve used this for sticky toolbars.
Example “portal-style” fixed dropdown:
const dd = document.querySelector('.dropdown');
const rect = btn.getBoundingClientRect();
dd.style.position = 'fixed';
dd.style.left = rect.left + 'px';
dd.style.top = rect.bottom + 'px';
dd.style.zIndex = 2000;
It stays glued to the button, even when the page moves.
For teams working in more specialised niches—say you’re optimising a casual-dating or hookup platform where the first-click experience determines whether a