Saya scroll sepanjang lima layar penuh dan tidak melihat satu pun foto. Empat foto yang mestinya saling berpudar mengikuti posisi scroll tidak pernah muncul, dan yang saya lewati cuma ruang kosong sepanjang itu.
Konsol bersih. Network tab menunjukkan semua fotonya ter-download dengan status 200. File-nya sampai, cuma tidak ada yang menampilkannya.
Gejalanya: tinggi ada, isinya tidak
Ini bagian dari halaman fitur smart home di sebuah situs klien yang saya kerjakan. Konsepnya sederhana di atas kertas: satu section setinggi 500vh, isinya sticky, lalu empat foto siklus siang ke malam yang saling cross fade mengikuti posisi scroll. Klien suka sekali ide ini, jadi dia harus jalan.
Yang terjadi waktu pertama kali dirender: scroll-nya panjang, sticky-nya nempel, tapi panggungnya kosong melompong. Tidak ada foto satu pun. Bukan gambar rusak, bukan ikon broken image, benar-benar tidak ada apa-apa.
Waktu saya buka DevTools dan pilih elemen foto pertamanya, angkanya langsung menjelaskan segalanya. Lebarnya ada, tingginya nol. Elemennya hadir di DOM, hanya saja dia tidak menempati ruang sama sekali.
Akar masalah pertama: object-fit ditempel ke <source>
Markup fotonya waktu itu berbentuk <picture> klasik, dengan beberapa <source> yang dipisah pakai media, lalu satu <img> sebagai fallback di paling bawah:
<div class="day-cycle__stage">
<picture class="day-cycle__photo">
<source media="(min-width: 1200px)" srcset="pagi_w1920.webp">
<source media="(min-width: 720px)" srcset="pagi_w1280.webp">
<img src="pagi_w720.webp" alt="">
</picture>
</div>Dan CSS-nya, ini bagian yang bikin saya menyalahkan diri sendiri, menargetkan <source>:
/* Tidak pernah melakukan apa-apa */
.day-cycle__photo source {
width: 100%;
height: 100%;
object-fit: cover;
}Aturan itu ditulis, di-parse browser, muncul rapi di panel Styles, dan efeknya nol. Alasannya: <source> bukan elemen yang dirender. Dia cuma metadata. Tugasnya memberi tahu browser kandidat file mana yang harus diambil, lalu setelah itu dia menghilang dari alur layout. Kotaknya tidak pernah ada, jadi width, height, dan object-fit di sana sama saja dengan menulis catatan di kertas yang langsung dibuang.
Yang membuat ini jahat adalah DevTools tidak protes. Tidak ada coretan seperti properti yang di-override, tidak ada warning. Selektornya valid, propertinya valid, targetnya saja yang salah jenis elemen.
Akar masalah kedua: <picture> itu display:inline
Kalaupun saya benarkan selektornya ke img, foto-fotonya tetap tidak akan muncul dengan benar. Karena ada masalah kedua yang menumpuk di atasnya.
<picture> bukan pembungkus ajaib. Dia elemen biasa dengan display: inline sebagai bawaan. Jadi ketika saya memberi class .day-cycle__photo ke <picture> dan berharap dia jadi lapisan absolut setinggi panggung, saya sebenarnya menyuruh sebuah kotak inline berperilaku seperti blok. Sementara <img> yang di dalamnya, yang sebenarnya satu-satunya elemen yang benar-benar dirender, tidak diberi tinggi apa pun.
Gabungan dua hal ini menghasilkan efek yang persis seperti yang saya lihat: aturan tinggi mendarat di elemen yang tidak dirender, elemen yang dirender tidak diberi tinggi, dan lapisan fotonya kolaps jadi nol.
Perbaikannya: buang <picture>, pakai srcset di <img>
Begitu penyebabnya jelas, solusinya justru menyederhanakan. <picture> dengan media itu berguna kalau kamu memang butuh art direction, misalnya crop yang berbeda untuk mobile dan desktop. Di sini saya tidak butuh itu. Yang saya butuhkan cuma satu foto yang sama dalam tiga ukuran, dan browser memilih sendiri yang paling pas.
Untuk kebutuhan itu, srcset dengan deskriptor lebar sudah cukup, dan cuma menyisakan satu elemen yang benar-benar dirender:
<img
class="day-cycle__photo"
src="pagi_w1280.webp"
srcset="pagi_w720.webp 720w,
pagi_w1280.webp 1280w,
pagi_w1920.webp 1920w"
sizes="100vw"
alt=""
>sizes="100vw" di sini jujur, karena fotonya memang selebar viewport. Dari situ browser menghitung sendiri mana kandidat yang paling masuk akal untuk lebar layar dan DPR perangkatnya.
Lalu stylingnya sekarang menempel langsung ke elemen yang punya kotak nyata:
.day-cycle__photo {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}display: block di sana bukan hiasan. <img> juga inline secara bawaan, dan saya lebih suka menuliskannya eksplisit daripada bergantung pada position: absolute yang kebetulan mem-blockify elemennya. Sekali ditulis, tidak ada lagi yang perlu diingat waktu class ini dipakai ulang di tempat lain.
Tiga hal lain yang ikut saya benahi
Setelah fotonya muncul, saya sekalian menutup beberapa lubang yang sebenarnya sudah menunggu giliran untuk jadi bug berikutnya.
Tinggi section pakai fallback sebelum svh. Unit svh enak dipakai di mobile karena tidak ikut goyang saat address bar browser menyusut, tapi tidak semua browser lama mengenalnya. Kalau nilai satu-satunya tidak dikenali, deklarasinya dibuang dan section-nya kehilangan tinggi sepenuhnya. Jadi tulis dua-duanya:
.day-cycle {
height: 500vh;
height: 500svh;
}Browser lama berhenti di baris pertama, browser baru menimpanya dengan baris kedua. Tidak butuh @supports.
Wrapper sticky dan panggung diberi ukuran eksplisit. height: 100% hanya bekerja kalau induknya punya tinggi yang bisa dirujuk. Rantai itu gampang putus di tengah tanpa terasa, dan gejalanya sama persis dengan bug tadi: anak yang absolut kolaps jadi nol. Jadi setiap tingkat sampai ke panggung fotonya saya tulis ukurannya:
.day-cycle__sticky,
.day-cycle__stage {
width: 100%;
height: 100%;
}Urutan lapisan ditulis eksplisit. Sebelumnya saya menyandarkan tumpukan pada urutan DOM saja. Begitu foto-fotonya jadi absolut semua, urutan itu jadi rapuh. Sekarang veil gelapnya dan konten teksnya masing-masing punya nomor:
.day-cycle__veil { z-index: 1; } /* di atas foto */
.day-cycle__content { z-index: 2; } /* di atas veil */Foto tetap di lapisan dasar tanpa z-index, veil menumpuk di atasnya untuk menjaga kontras teks, dan kontennya paling atas.
Yang saya bawa pulang
<source>tidak dirender. Apa pun yang kamu tulis untuk elemen itu, mauwidth,height, atauobject-fit, tidak akan pernah punya efek visual. Styling gambar responsif selalu menempel ke<img>.<picture>bukan pembungkus layout. Bawaannyadisplay: inline, jadi memberinya class posisi dan ukuran tanpa mengubahdisplaycuma mengundang masalah.- Kalau yang kamu butuhkan cuma satu gambar dalam beberapa resolusi,
srcsetplussizesdi<img>biasa sudah cukup. Simpan<picture>untuk saat kamu benar-benar butuh art direction. - Elemen yang tingginya nol di DevTools itu petunjuk paling cepat. Begitu angka tingginya nol padahal aturan CSS-nya kelihatan aktif, pertanyaannya bukan "kenapa aturannya tidak jalan" tapi "apakah aturan ini mendarat di elemen yang benar".
- Untuk unit viewport modern, selalu tulis
vhdulu barusvh. Deklarasi yang tidak dikenali akan dibuang diam-diam, dan kamu tidak mau tinggi section-mu hilang cuma karena satu unit.