Gerbang Sampul & Snap-Scroll
Pola penguncian layar pembuka menggunakan CSS Scroll Snap sebelum pengguna menekan tombol buka undangan.
Membaca sekitar 2 menit
Pola penguncian layar pembuka menggunakan CSS Scroll Snap sebelum pengguna menekan tombol buka undangan.
Menahan tamu di dua seksi pembuka (Hero dan kartu sapaan) dengan snap-y mandatory, lalu melepas kunci layar ke mode gulir bebas menggunakan AnimatePresence saat tombol OPEN INVITATION ditekan.
Perpindahan dari kontainer snap-scroll ke halaman panjang membutuhkan pengaturan ulang posisi scroll tepat di jeda animasi agar halaman tidak melompat secara kasar.
Masalahnya dulu
Undangan digital punya ritual pembuka: tamu melihat sampul depan terlebih dahulu, membaca kepada siapa undangan ditujukan, baru kemudian menekan tombol 'Buka Undangan' untuk masuk ke isi acara. Kalau seluruh halaman langsung bisa digulir dari atas sampai bawah sejak awal, kejutan visual dan alur ceritanya langsung hilang.
Menggabungkan CSS Scroll Snap dengan gerbang state
Di HomePage.tsx, alur halaman dibagi menjadi dua dunia yang dikendalikan oleh state isOpen. Saat isOpen masih bernilai false, pengguna berada di dalam wadah setinggi layar penuh (h-screen) dengan aturan snap-y snap-mandatory. Di dalamnya hanya ada Seksi 1 (Hero dengan tanggal perjalanan hubungan) dan Seksi 2 (kartu sapaan beserta tombol OPEN INVITATION). Begitu tombol ditekan, AnimatePresence dengan mode="wait" akan menutup sampul dan merender Seksi 3 hingga Seksi 7 dalam mode gulir bebas.
Sampul undangan harus terasa seperti amplop fisik: ada batas yang jelas antara melihat bagian luar dan membuka isinya.
Implementasi di ngodingan ini
Berikut adalah potongan kode di src/pages/HomePage.tsx yang mengatur transisi antara mode terkunci (closed) dan mode terbuka (open):
<AnimatePresence mode="wait">
{!isOpen && (
<motion.div
key="closed"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, y: -30 }}
transition={{ duration: 0.8 }}
className="h-screen overflow-y-scroll snap-y snap-mandatory"
style={{ scrollbarWidth: "none" }}
>
{/* SECTION 1 — Hero & SECTION 2 — Open Invitation */}
<button
onClick={() => {
setTimeout(
() => window.scrollTo({ top: 0, behavior: "instant" }),
600
);
setTimeout(() => setIsOpen(true), 600);
}}
className="cursor-pointer w-fit px-4 py-2 rounded bg-red-900 text-white text-xs font-semibold"
>
OPEN INVITATION
</button>
</motion.div>
)}
</AnimatePresence>Jebakan dan detail yang harus dijaga
- Jika posisi scroll tidak di-reset ke atas (window.scrollTo({ top: 0, behavior: "instant" })) saat berpindah ke mode terbuka, pengguna bisa mendarat di tengah-tengah Seksi 3 karena peramban mempertahankan posisi gulir sebelumnya.
- Penundaan 600ms sebelum mengubah state setIsOpen(true) diselaraskan dengan durasi transisi keluar agar pergantian DOM tidak memotong animasi memudar pada sampul.
- Menyembunyikan scrollbar bawaan (scrollbarWidth: "none" dan ::-webkit-scrollbar { display: none }) sangat penting di mode snap-scroll supaya tampilan menyerupai aplikasi native, bukan dokumen kaku.