Grid Dua Kolom di Mobile, Satu Keputusan yang Gue Tarik Balik
Grid 4 kolom dipecah 2 di layar kecil bikin kartu mepet. Solusinya: carousel embla di mobile, grid tetap di desktop, dan DOM jadi dobel.
Pas buka halaman services di layar iPhone punya klien, yang kelihatan cuma dua kartu sempit ngepas di tepi. Judulnya aja kayak mau meluber keluar. Ini bukan salah desain klien, walau dugaan pertama gue memang ke situ. Hasil keputusan gue sendiri di commit sebelumnya: grid 4 kolom di desktop gue pecah jadi grid-cols-2 gap-3 pas layar kecil, modalnya "biar dari 4 jadi 2 aja, kan lebih lega". Nyatanya di 375 piksel, tiap kartu cuma dapat separuh lebar layar dikurangi padding container. Mepet.
Awalnya gue kira ini cuma soal spacing. Kecilin font, hapus gap, selesai. Tapi pas gue inspect element, masalahnya bukan ukuran huruf — rasio lebar kartu terhadap isinya emang nggak nyambung. Thumbnail, judul dua baris, deskripsi pendek. Semuanya kegenerasi lebar 45% layar. Grid dua kolom itu keputusan yang salah buat konten begini.
Mobile diganti carousel, desktop tetep grid
Di commit 321dfea gue ubah pendekatannya. Mobile nggak pakai grid lagi, diganti carousel swipe pakai embla-carousel-react lewat komponen Carousel dari shadcn/ui. Desktop tetep grid, cuma sekarang beda render: satu halaman punya dua struktur untuk konten yang sama.
{/* Mobile: Carousel */}
<div className="block sm:hidden -mx-4 px-4">
<Carousel opts={{ align: "start" }} className="w-full">
<CarouselContent className="-ml-4">
{detail.coreServices.map((service) => (
<CarouselItem key={service.title} className="pl-4 basis-[85%]">
<ServiceCard service={service} className="h-full" />
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
{/* Desktop: Grid */}
<Reveal stagger staggerAmount={0.1} className="hidden sm:grid sm:grid-cols-2 sm:gap-5 lg:grid-cols-4">
{detail.coreServices.map((service) => (
<ServiceCard key={service.title} service={service} />
))}
</Reveal>
Detail kecil yang paling ngefek: basis-[85%] di CarouselItem. Kalau satu slide ngepas persis selebar layar, orang nggak bakal tahu ada kartu lain di sebelahnya. Sisa 15% yang kelihatan ujungnya adalah hint visual "ini bisa digeser". Pembungkus -mx-4 px-4 itu trik full-bleed: margin negatif narik carousel keluar dari padding container bawaan layout, terus padding yang sama dipasang lagi di dalam supaya isi kartu nggak nempel pinggir layar.
Komponen carousel.tsx dari shadcn itu "use client" karena di dalamnya ada hook useEmblaCarousel. Dipanggil dari page.tsx yang server component, pola client island yang sama kayak waktu gue bahas Show More dan trade-off SEO-nya di komponen services yang sama. HTML awal tetep dirender di server, interaktivitas swipe-nya aja yang aktif di sisi client.
Satu hal yang gampang kelewat: akses keyboard. File carousel.tsx bawaan shadcn udah nyediain handler ArrowLeft/ArrowRight buat geser slide, plus CarouselPrevious/CarouselNext kalau mau tombol navigasi eksplisit. Detail begini yang bikin gue milih pakai komponen siap pakai ketimbang nulis wrapper sendiri di atas useEmblaCarousel — urusan perilaku dasar udah diurus orang, gue tinggal sesuaikan style sama lebar slide-nya.
Gue ngecek sumber resmi dulu sebelum nambah dependency. embla-carousel-react versi 8.6.0 udah stable, lisensi MIT, dengan sekitar 6,2 juta weekly downloads di npm (npm). Versi 9 masih release candidate dan ada migration guide v8 ke v9 di halaman releases, jadi gue pegang v8 aja dulu. Wrapper React-nya yang bikin betah: automatic cleanup on unmount, jadi nggak ada event listener nganggur nggantung pas komponen hilang (docs embla). Komponen carousel shadcn juga emang dibangun di atas embla-carousel-react (shadcn docs).
Harga yang gue bayar: DOM dobel
Trade-off jujurnya: konten yang sama dirender dua kali di DOM. Carousel di block sm:hidden, grid di hidden sm:grid. Yang tampil salah satu, yang lain cuma disembunyiin lewat class Tailwind. Dua-duanya tetep ada di HTML hasil server render, jadi aman buat mobile-first indexing; kontennya tetep kebaca crawler, nggak disembunyiin dari HTML. Kalau besok klien minta ganti teks kartu, jangan cuma edit satu tempat — bisa ketipu karena yang kelihatan di mobile sama di desktop itu elemen beda.
Carousel bukan jawaban universal. Kalo isinya cuma dua-tiga kartu, grid satu kolom masih paling enak dibaca, scroll vertikal biasa aja. Gue baru pilih carousel pas itemnya banyak dan grid bikin kartu kekecilan, kayak kejadian di halaman services ini. Pas nambah kartu baru nanti, ingat: ada dua tempat yang harus diisi.