Halaman search di proyek listing yang saya kerjakan punya header sticky di atas dan peta Leaflet besar di bawahnya. Semua terlihat normal sampai saya scroll. Begitu peta melewati header, tombol zoom Leaflet dan beberapa marker tiba-tiba muncul di atas nav. Tombol + / - peta nangkring menutupi logo, dan satu marker menggantung di depan menu seolah dia bagian dari header.
Tidak ada error di console. Tidak ada warning. Hanya layout yang salah secara visual. Awalnya saya kira posisinya yang ngaco, tapi header-nya sendiri baik-baik saja — yang naik ke depan justru elemen internal Leaflet. Header saya kira-kira begini:
.site-header {
position: sticky;
top: 0;
z-index: 200;
}Z-index 200 itu menurut saya sudah lebih dari cukup tinggi untuk sebuah header. Ternyata tidak, dan alasannya bukan soal angka.
Kenapa ini terjadi
Leaflet datang dengan CSS vendor sendiri, dan CSS itu memberi z-index yang sangat tinggi ke elemen-elemen internalnya. .leaflet-pane dan .leaflet-control punya nilai z-index sampai kisaran 800 hingga 1000. Itu desain yang masuk akal dari sisi Leaflet — mereka butuh kontrol selalu di atas tile dan marker selalu di atas overlay, jadi mereka pasang angka besar biar aman di dalam petanya sendiri.
Masalahnya muncul karena container peta saya tidak membentuk stacking context sendiri. Tanpa stacking context, semua z-index internal Leaflet itu tidak benar-benar "di dalam" peta — mereka ikut bertanding di stacking context yang sama dengan header saya, yaitu root.
Jadi yang sebenarnya terjadi adalah perbandingan langsung: kontrol Leaflet di z-index ~1000 versus header saya di z-index 200. Di context yang sama, 1000 menang telak. Header kalah, dan kontrol peta nembus ke depannya. Angka 200 saya bukannya kurang tinggi — dia cuma sedang adu di arena yang salah.
Insting pertama yang salah adalah menaikkan z-index header. Saya naikkan ke 1100, sempat kelihatan beres, lalu sadar ini jebakan. Leaflet bisa saja punya elemen lain di z-index lebih tinggi, atau plugin pihak ketiga lain ikut bertarung. Saya akan terjebak dalam perlombaan angka yang tidak ada ujungnya — setiap widget baru memaksa saya menaikkan header lagi. Itu bukan perbaikan, itu menunda masalah.
Perbaikannya
Kuncinya bukan menang adu angka, tapi membatasi arena adu-nya. Saya beri container peta stacking context-nya sendiri. Begitu peta jadi stacking context, semua z-index internal Leaflet terkurung di dalamnya dan tidak bisa lagi dibandingkan langsung dengan header.
.leaflet-container {
isolation: isolate;
position: relative;
z-index: 0;
}isolation: isolate adalah cara paling bersih untuk memaksa sebuah elemen membuat stacking context baru tanpa efek samping visual lain. position: relative dengan z-index: 0 memperkuat itu dan menempatkan keseluruhan peta di z-index 0 relatif terhadap header.
Setelah ini, hierarki-nya jadi jelas dan benar:
- Kontrol Leaflet boleh saja tetap di z-index 1000 — tapi 1000 itu sekarang di dalam peta.
- Keseluruhan peta hanya menempati z-index 0 di stacking context root.
- Header di z-index 200 berada di context root yang sama, dan 200 > 0, jadi header selalu di atas.
Z-index internal Leaflet tidak saya sentuh sama sekali. Saya tidak melawan vendor-nya, saya cuma menaruh seluruh peta ke dalam sebuah kotak. Apa pun yang terjadi di dalam kotak itu — entah 800, entah 1000 — tidak bisa lagi keluar dan menabrak header. Begitu scroll lagi, tombol zoom dan marker lewat dengan rapi di bawah nav, persis seperti yang saya harapkan dari awal.
Yang saya suka dari pendekatan ini: perbaikannya cuma tiga baris di satu selector, tidak mengubah perilaku Leaflet, dan tahan banting terhadap plugin atau update Leaflet di masa depan. Selama peta tetap punya stacking context-nya sendiri, angka berapa pun di dalamnya tidak jadi urusan header.
Pelajaran
Z-index yang tinggi dari widget pihak ketiga bukan ajakan untuk adu angka. Begitu kamu merasa harus terus menaikkan z-index sebuah elemen biar tetap di atas, itu sinyal kamu sedang bertarung di stacking context yang salah — bukan kekurangan angka.
Jalan yang benar adalah membungkus widget itu dalam stacking context-nya sendiri pakai isolation: isolate. Begitu z-index internalnya terkurung, dia tidak bisa lagi bocor keluar dan bersaing dengan elemen kamu yang lain. Kamu mengatur satu angka yang bersih untuk seluruh widget, lalu lupakan apa pun yang terjadi di dalamnya.
Aturan praktis yang saya pegang sekarang: jangan pernah berusaha mengalahkan z-index milik widget vendor. Kotakkan dia. Stacking context itu pagar, bukan tangga — dan dalam kasus seperti ini, pagar jauh lebih kamu butuhkan.
