D
P
0

CSS & Web Animation

Leaflet Map Controls Leak Above Your Sticky Header — Fix It With a Stacking Context

June 26, 2026·4 min read
Leaflet Map Controls Leak Above Your Sticky Header — Fix It With a Stacking Context

The search page on a listing project I built had a sticky site header at the top and a large Leaflet map below it. Everything looked fine until I scrolled. The moment the map slid up under the header, Leaflet's zoom controls and a couple of markers suddenly rendered above the nav. The map's + / - buttons sat on top of the logo, and one marker floated in front of the menu as if it belonged to the header.

No console errors. No warnings. Just a visually broken layout. My first guess was that something was positioned wrong, but the header itself was fine — what climbed in front of it was Leaflet's own internal furniture. The header looked roughly like this:

.site-header {
  position: sticky;
  top: 0;
  z-index: 200;
}

A z-index of 200 felt more than high enough for a header. It wasn't, and the reason had nothing to do with the number.

Why this happens

Leaflet ships its own vendor CSS, and that CSS assigns very high z-index values to its internal elements. .leaflet-pane and .leaflet-control get z-index values up in the 800 to 1000 range. That's a reasonable design on Leaflet's side — controls always need to sit above tiles, and markers above overlays, so they reach for big numbers to stay safe inside the map.

The trouble started because my map container didn't form its own stacking context. Without one, all of Leaflet's internal z-indexes aren't really "inside" the map at all — they compete in the same stacking context as my header, which is the root.

So what was actually happening was a head-to-head comparison: Leaflet's controls at z-index ~1000 versus my header at z-index 200. In the same context, 1000 wins outright. The header loses, and the map controls leak in front of it. My 200 wasn't too low — it was just fighting in the wrong arena.

The wrong first instinct is to raise the header's z-index. I bumped it to 1100, it looked fixed for a second, and then I realized it was a trap. Leaflet could have another element at an even higher value, or some other third-party plugin could join the brawl. I'd be stuck in an arms race with no ceiling — every new widget forcing me to push the header up again. That isn't a fix, it just postpones the problem.

The fix

The point isn't to win the number contest — it's to contain the contest. I gave the map container its own stacking context. Once the map is a stacking context, all of Leaflet's internal z-indexes are trapped inside it and can no longer be compared directly against the header.

.leaflet-container {
  isolation: isolate;
  position: relative;
  z-index: 0;
}

isolation: isolate is the cleanest way to force an element to create a new stacking context with no other visual side effects. position: relative with z-index: 0 reinforces that and parks the whole map at z-index 0 relative to the header.

After this, the hierarchy is clear and correct:

  • Leaflet's controls can stay at z-index 1000 — but that 1000 is now inside the map.
  • The whole map occupies just z-index 0 in the root stacking context.
  • The header sits at z-index 200 in that same root context, and 200 > 0, so the header is always on top.

I never touched Leaflet's internal z-indexes at all. I'm not fighting the vendor — I'm just putting the entire map inside a box. Whatever happens in that box, whether it's 800 or 1000, can no longer escape and crash into the header. Scroll again and the zoom buttons and markers slide neatly under the nav, exactly the way I expected from the start.

What I like about this approach: the fix is three lines on a single selector, it changes none of Leaflet's behavior, and it's resilient against future plugins or Leaflet updates. As long as the map keeps its own stacking context, whatever number lives inside it is none of the header's business.

The takeaway

A high z-index from a third-party widget is not an invitation to out-bid it. The moment you feel you have to keep raising an element's z-index to stay on top, that's the signal you're fighting in the wrong stacking context — not that your number is too small.

The right move is to wrap that widget in its own stacking context with isolation: isolate. Once its internal z-indexes are contained, they can't leak out and compete with the rest of your UI. You set one clean number for the whole widget and forget about everything happening inside it.

The rule I carry now: never try to beat a vendor widget's z-index. Box it. A stacking context is a fence, not a ladder — and in cases like this, the fence is exactly what you need.