Mosaik Presisi & Lightbox Galeri
Tata letak CSS Grid 3 kolom untuk 20 foto dengan pemetaan koordinat eksplisit tanpa celah kosong, dipadukan dengan penampil Lightbox layar penuh.
Membaca sekitar 2 menit
Tata letak CSS Grid 3 kolom untuk 20 foto dengan pemetaan koordinat eksplisit tanpa celah kosong, dipadukan dengan penampil Lightbox layar penuh.
Setiap foto di GalleryPage diberi koordinat [colStart, rowStart, colSpan, rowSpan] dari tabel PLACEMENTS, nilai objectPosition khusus agar wajah tidak terpotong, serta filter warna seragam.
Menyusun 20 foto dengan ukuran campuran (2x2, 1x2, 1x1) secara matematis di atas 12 baris grid agar membentuk persegi panjang utuh tanpa satu pun sel yang bolong.
Masalahnya dulu
Menampilkan 20 foto pre-wedding dalam grid seragam berukuran sama sering kali terlihat membosankan. Namun jika kita mencampur ukuran foto besar (2x2), tinggi (1x2), dan kecil (1x1) secara acak di CSS Grid, sering muncul celah kosong (gap bolong) atau urutan foto yang berantakan. Selain itu, rasio kotak yang bervariasi rawan memotong bagian wajah mempelai jika menggunakan object-fit: cover standar.
Pemetaan koordinat eksplisit & titik fokus per foto
Di halaman app/gallery/page.tsx, saya menyusun matriks PLACEMENTS berisi 20 tupel [colStart, rowStart, colSpan, rowSpan] di atas grid 3 kolom x 12 baris. Setiap sel terisi rapat seperti kepingan Tetris. Selain itu, setiap item di dalam array PHOTOS memiliki konfigurasi titik potong (crop / objectPosition) dan filter warna (brightness, saturate, sepia) masing-masing agar tone warnanya menyatu dengan tema emas-gelap undangan.
Grid mosaik yang rapi bukan hasil kebetulan algoritma auto-placement, melainkan hasil pemetaan koordinat baris dan kolom yang dihitung presisi.
Implementasi di ngodingan ini
Berikut adalah potongan matriks penempatan mosaik beserta cara komponen merender setiap bingkai foto dan menangani navigasi swipe di dalam Lightbox:
const PLACEMENTS: [number, number, number, number][] = [
[1, 1, 2, 2], // 0: big 2x2 (baris 1-2, kolom 1-2)
[3, 1, 1, 2], // 1: tall 1x2 (baris 1-2, kolom 3)
[1, 3, 1, 2], // 2: tall 1x2 (baris 3-4, kolom 1)
[2, 3, 1, 1], // 3: small 1x1 (baris 3, kolom 2)
[3, 3, 1, 1], // 4: small 1x1 (baris 3, kolom 3)
// ... tersusun rapat hingga baris ke-12 untuk 20 foto
];
// Render item di dalam .gal-grid:
const [cs, rs, csp, rsp] = PLACEMENTS[i];
return (
<div
key={photo.id}
className={`gi${csp >= 2 ? " wide" : ""}`}
onClick={() => setActive(i)}
style={{
gridColumn: `${cs} / span ${csp}`,
gridRow: `${rs} / span ${rsp}`,
animationDelay: `${Math.min(i * 0.045, 0.6)}s`,
}}
>
<img
src={photo.src}
alt={photo.label}
className="gi-img"
loading={i < 6 ? "eager" : "lazy"}
style={{ objectPosition: photo.crop, filter: photo.filter }}
/>
</div>
);Jebakan dan detail yang harus dijaga
- Enam foto pertama diberi atribut loading='eager' agar area paruh atas layar (above the fold) langsung tampil instan, sedangkan 14 foto sisanya dimuat secara malas (lazy) mengikuti guliran layar.
- Header galeri menggunakan deteksi arah scroll (y < lastY.current || y < 80): otomatis bersembunyi ke atas saat pengguna fokus menggulir foto ke bawah, dan langsung muncul kembali begitu pengguna menggulir sedikit saja ke atas.