- Home
- Kelas Belajar
- CSS Layout Modern: Kuasai Flexbox, Grid & Responsive Design untuk Produksi
Kelas menengah ini membawa kamu dari pemahaman dasar CSS layout menuju penerapan production-like dengan Flexbox, CSS Grid, responsive design, dan animasi. Fokus bukan hanya "caranya" tapi "kapan dan kenapa" setiap pola dipakai, lengkap dengan best practice dan kesalahan umum di dunia kerja. Di akhir kelas, kamu akan menggabungkan semua konsep menjadi satu komponen UI kompleks yang siap pakai.
Sebelum menulis satu baris display: flex pun, developer produksi selalu menyiapkan fondasi CSS terlebih dahulu. Ini bukan basa-basi — struktur file yang berantakan dan absennya reset/custom properties adalah penyebab utama kenapa CSS di banyak proyek jadi "spageti" setelah beberapa bulan dikembangkan banyak orang. Modul ini fokus pada tiga fondasi: struktur file yang scalable, reset/normalize yang tepat guna, dan custom properties sebagai sistem desain.
Untuk proyek kelas menengah ke atas, satu file style.css raksasa akan sulit di-maintain. Pola yang umum dipakai di industri adalah pemisahan berdasarkan tanggung jawab (separation of concerns), bukan berdasarkan halaman:
styles/
base/
_reset.css
_variables.css
_typography.css
layout/
_grid.css
_navbar.css
components/
_card.css
_button.css
utilities/
_spacing.css
main.css
Prinsipnya: base berisi aturan fondasi yang berlaku global (reset, variabel, tipografi dasar), layout mengatur struktur besar halaman (grid utama, header, sidebar), components berisi unit UI yang bisa dipakai ulang (card, button, modal), dan utilities berisi class kecil serbaguna (misalnya .mt-4, .text-center). Di produksi, file-file ini biasanya digabung lewat @import saat development atau di-bundle oleh build tool (Vite, PostCSS) untuk production.
Kesalahan umum: menaruh style komponen spesifik (misalnya .hero-banner) di file layout, atau sebaliknya. Akibatnya, saat komponen dipindah/dihapus, developer lain kesulitan menemukan CSS terkait. Aturan sederhana: jika sebuah blok CSS mendeskripsikan "apa" (button, card), taruh di components; jika mendeskripsikan "di mana" (header, sidebar, grid utama), taruh di layout.
Setiap browser punya user agent stylesheet default yang berbeda-beda (margin body, ukuran heading, list style, dsb). Tanpa penyeragaman, layout Flexbox dan Grid yang kamu bangun bisa terlihat berbeda di tiap browser karena box model awalnya sudah tidak konsisten.
Untuk kelas ini, kita pakai pendekatan reset modern yang ringkas dan relevan untuk layout Flexbox/Grid:
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
min-height: 100vh;
line-height: 1.5;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
box-sizing: border-box di sini krusial: tanpa ini, perhitungan lebar elemen di Grid (misalnya grid-template-columns: 1fr 1fr) bisa meleset karena padding dan border ditambahkan di luar lebar yang kamu tentukan, bukan di dalamnya. Ini salah satu penyebab bug layout "kotak lebih lebar dari kolom grid-nya" yang sering bikin bingung pemula.
CSS Custom Properties (--nama-variabel) sering disalahpahami sebagai pengganti Sass variable. Padahal keunggulan utamanya adalah bisa diubah secara dinamis di runtime dan mengikuti cascade/scope — dua hal yang tidak bisa dilakukan Sass variable karena itu di-compile-time.
:root {
--color-primary: #2563eb;
--color-text: #1e293b;
--color-bg: #ffffff;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-4: 1rem;
--space-8: 2rem;
--radius-md: 0.5rem;
--font-base: 1rem;
--breakpoint-md: 768px;
}
Definisikan token untuk warna, spacing, radius, dan tipografi di :root agar konsisten dipakai di seluruh komponen. Ini menjadi dasar penting untuk modul-modul berikutnya, terutama saat kita membahas responsive design dan container queries — custom properties bisa di-override per breakpoint atau per komponen tanpa duplikasi logic.
Contoh scoping lokal yang sering dipakai di produksi: override variabel di dalam komponen tertentu tanpa mengubah nilai global.
.card {
--card-padding: var(--space-4);
padding: var(--card-padding);
border-radius: var(--radius-md);
background: var(--color-bg);
}
.card--compact {
--card-padding: var(--space-2);
}
Pola ini jauh lebih maintainable dibanding membuat class baru untuk setiap variasi padding, karena logic-nya tetap terpusat pada satu custom property.
--primaryColor dan --color-secondary) sehingga tim kesulitan menghafal pola.var() untuk kasus custom property belum terdefinisi: color: var(--color-primary, #000).Bayangkan kamu baru bergabung sebagai frontend developer di startup dan diminta membangun landing page baru. Tim design sudah memberikan style guide sederhana: warna primary #2563eb, warna teks #1e293b, spacing berbasis kelipatan 4px, dan radius tombol 8px. Sebelum coding komponen, kamu diminta menyiapkan fondasi CSS agar developer lain bisa langsung lanjut mengerjakan komponen tanpa bentrok konvensi.
Langkah yang bisa kamu coba sendiri:
base/, layout/, components/, utilities/ seperti contoh di atas._reset.css menggunakan reset minimal yang sudah dibahas, pastikan box-sizing: border-box ada._variables.css berisi custom properties sesuai style guide (warna, spacing kelipatan 4px sampai 32px, radius, breakpoint).main.css dengan urutan: reset lebih dulu, lalu variables, baru file lain — urutan ini penting karena custom properties di :root harus tersedia sebelum dipakai komponen manapun.var(--color-primary) dan var(--space-4), lalu ubah nilai di :root untuk membuktikan perubahan langsung merambat ke semua komponen tanpa mengubah kode komponen itu sendiri.Jika langkah terakhir berhasil — satu perubahan variabel global merombak tampilan di banyak tempat sekaligus — berarti fondasi CSS kamu sudah siap dipakai untuk membangun layout Flexbox dan Grid yang akan kita bahas di modul-modul berikutnya.
Di modul sebelumnya kamu sudah menyiapkan fondasi proyek — struktur file, reset CSS, dan custom properties. Sekarang kita masuk ke bagian yang paling sering dipakai sehari-hari sebagai developer front-end: Flexbox. Tapi bukan Flexbox versi "display: flex terus beres", melainkan pemahaman mendalam soal bagaimana elemen benar-benar mengambil ruang, kapan alignment jadi rumit, dan bagaimana menghindari bug layout yang baru muncul saat konten berubah-ubah (yang di dunia kerja itu terjadi terus-menerus).
Kita anggap kamu sudah familiar dengan display: flex, justify-content, dan align-items dasar. Yang akan kita bahas di sini adalah area yang sering bikin developer menengah stuck: kombinasi flex-grow, flex-shrink, flex-basis, alignment bertingkat (multi-line), dan kasus di mana flex container berperilaku "aneh" karena konten dinamis.
flexProperti flex sebenarnya shorthand dari tiga nilai:
.item {
flex: ;
}
/* contoh */
.item {
flex: 1 1 200px;
}
width, tapi sering berperan seperti itu di flex row.Kesalahan umum: developer menulis width: 200px pada flex item, padahal flex-basis punya prioritas lebih tinggi dalam konteks flex. Kalau kamu set flex: 1 1 auto sambil masih pakai width, hasilnya bisa membingungkan karena width dianggap sebagai basis fallback saja, bukan aturan mutlak.
flex: 1; /* sama dengan flex: 1 1 0% */
flex: auto; /* sama dengan flex: 1 1 auto */
flex: none; /* sama dengan flex: 0 0 auto */
flex: initial; /* sama dengan flex: 0 1 auto (default) */
Perbedaan flex: 1 vs flex: auto sering jadi sumber bug: flex: 1 mengabaikan ukuran konten asli dan langsung membagi rata sisa ruang (basis 0%), sedangkan flex: auto menghitung dari ukuran konten dulu baru sisanya dibagi. Kalau kamu punya tombol dengan teks panjang-pendek berbeda dan ingin lebar seragam, pakai flex: 1. Kalau ingin lebar mengikuti konten tapi tetap bisa membesar, pakai flex: auto.
Saat flex container punya flex-wrap: wrap, kamu berurusan dengan dua sumbu alignment yang sering tertukar:
align-items — mengatur alignment item di dalam satu baris (cross-axis, per item).align-content — mengatur jarak antar baris ketika ada lebih dari satu baris (cross-axis, untuk keseluruhan baris).align-content tidak akan berefek apa pun kalau flex container tidak wrap atau hanya punya satu baris. Ini penyebab paling umum kenapa developer merasa "align-content tidak berfungsi" — padahal itu memang sesuai spesifikasi.
.container {
display: flex;
flex-wrap: wrap;
align-content: space-between; /* efek hanya terlihat jika ada >1 baris & container punya extra height */
align-items: center; /* efek per item di dalam barisnya masing-masing */
}
align-selfKadang kamu butuh satu item berbeda alignment dari saudara-saudaranya, misal badge notifikasi yang harus menempel ke atas sementara item lain center. Gunakan align-self pada item tersebut, ia meng-override align-items dari parent hanya untuk elemen itu saja.
Pola klasik di dashboard: sidebar dengan lebar tetap, area konten mengisi sisa ruang, dan keduanya harus punya tinggi yang sama tanpa perlu di-set manual.
.layout {
display: flex;
min-height: 100vh;
}
.sidebar {
flex: 0 0 240px; /* tidak grow, tidak shrink, basis 240px tetap */
}
.content {
flex: 1; /* mengambil semua sisa ruang */
min-width: 0; /* mencegah overflow saat ada konten panjang seperti tabel/kode */
}
min-width: 0 di sini penting dan sering terlewat: default flex item punya min-width: auto, yang membuat item menolak mengecil lebih kecil dari ukuran konten intrinsiknya (misal teks panjang tanpa spasi atau tabel lebar). Akibatnya layout bisa overflow horizontal padahal secara logika harusnya shrink.
Bayangkan komponen search bar: input harus fleksibel mengisi ruang, tapi tombol "Cari" harus tetap pada ukurannya meski ruang menyempit.
.search-bar {
display: flex;
gap: 8px;
}
.search-bar input {
flex: 1 1 auto;
min-width: 0;
}
.search-bar button {
flex: 0 0 auto; /* tombol tidak ikut mengecil atau membesar */
}
Tanpa flex-shrink: 0 pada tombol, teks di dalamnya bisa terpotong saat container menyempit karena browser akan mencoba mengecilkan semua flex item secara proporsional, termasuk elemen yang seharusnya tetap.
margin: auto untuk push-to-end padahal bisa lebih terbaca dengan justify-content: space-between — keduanya valid, tapi margin: auto pada satu item flex efektif untuk kasus spesifik seperti "satu tombol logout terpisah dari menu lain".gap pada flex container jauh lebih aman dibanding margin antar item, karena tidak perlu trik :last-child untuk menghapus margin di ujung.min-width: 0 atau min-height: 0 pada level yang tepat, menyebabkan overflow yang sulit dilacak karena bug-nya "menular" ke parent.flex-basis dalam satuan % tanpa mempertimbangkan gap, sehingga total lebar melebihi 100% dan menyebabkan wrap yang tidak diinginkan.Kamu bergabung ke sebuah tim dan diberi tugas memperbaiki komponen toolbar berikut yang dilaporkan "berantakan" saat browser di-resize ke lebar sedang (tablet):
<div class="toolbar">
<div class="logo">BrandName</div>
<nav class="menu">
<a href="#">Dashboard</a>
<a href="#">Analytics</a>
<a href="#">Settings Panel Lanjutan</a>
</nav>
<button class="cta">Upgrade ke Pro</button>
</div>
<style>
.toolbar {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 24px;
}
.logo {
font-weight: bold;
}
.menu {
display: flex;
gap: 16px;
}
.cta {
padding: 8px 16px;
}
</style>
Bug yang dilaporkan: saat layar menyempit, tombol "Upgrade ke Pro" ikut mengecil dan teksnya terpotong, sementara menu navigasi malah tidak mau wrap sehingga toolbar overflow ke samping.
Langkah penyelesaian yang bisa kamu coba sendiri:
flex: 0 0 auto; pada .cta agar tidak pernah mengecil..menu nilai flex: 1 1 auto; min-width: 0; supaya ia mengisi ruang sisa tapi tetap boleh menyusut tanpa mendorong elemen lain..menu sendiri adalah flex container dengan beberapa link, tambahkan flex-wrap: wrap; pada .menu agar link bisa turun ke baris baru saat ruang benar-benar sempit, alih-alih memaksa overflow.white-space: nowrap; agar tidak pecah di tengah kata, dikombinasikan dengan wrap di level container agar link tersebut yang pindah baris, bukan teksnya yang terpotong.Hasil akhirnya, toolbar akan terasa "pintar": elemen penting (CTA) tetap terbaca utuh, sementara elemen sekunder (menu) mengalah dan menyesuaikan diri. Prinsip ini — menentukan mana yang boleh fleksibel dan mana yang harus tetap — adalah inti dari penggunaan Flexbox yang matang, dan akan terus kamu pakai saat membangun navbar dan card layout responsif di modul berikutnya.
Di dua modul sebelumnya kamu sudah paham cara kerja flex-grow, flex-shrink, flex-basis, dan berbagai kombinasi alignment. Sekarang saatnya menggabungkan semua itu menjadi dua komponen yang hampir pasti kamu temui di setiap proyek nyata: navbar dan card grid. Fokus modul ini bukan sekadar "bisa jalan", tapi bagaimana membuatnya tahan terhadap perubahan konten dan ukuran layar — dua hal yang sering bikin layout production berantakan.
Navbar terlihat simpel, tapi punya banyak edge case: logo dengan lebar tidak konsisten, menu yang bisa bertambah item, tombol CTA yang tidak boleh mengecil, dan kebutuhan collapse di mobile. Berikut struktur dasar yang tahan banting:
<nav class="navbar">
<a href="/" class="navbar__brand">Logo</a>
<ul class="navbar__menu">
<li><a href="#">Produk</a></li>
<li><a href="#">Harga</a></li>
<li><a href="#">Blog</a></li>
</ul>
<button class="navbar__cta">Daftar</button>
</nav>
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-3) var(--space-5);
}
.navbar__brand {
flex: 0 0 auto; /* jangan pernah mengecil atau membesar */
}
.navbar__menu {
display: flex;
gap: var(--space-4);
flex: 1 1 auto; /* mengisi ruang tengah */
justify-content: center;
list-style: none;
}
.navbar__cta {
flex: 0 0 auto;
white-space: nowrap; /* cegah teks tombol terpotong dua baris */
}
Perhatikan pola flex: 0 0 auto pada brand dan CTA — ini kunci agar dua elemen tersebut mempertahankan ukuran aslinya berapa pun sisa ruang yang ada, sementara menu di tengah yang menyerap sisa ruang lewat flex: 1 1 auto. Ini kebalikan dari kesalahan umum: banyak developer memberi flex: 1 ke semua child navbar, akibatnya logo dan tombol ikut menyusut saat menu bertambah panjang.
Masalah nyata: jika jumlah item menu bertambah (misal karena CMS dinamis), menu bisa overflow di layar sempit sebelum breakpoint mobile tercapai. Solusi cepat tanpa JavaScript adalah membiarkan menu scroll horizontal:
.navbar__menu {
overflow-x: auto;
scrollbar-width: none; /* sembunyikan scrollbar di Firefox */
}
.navbar__menu::-webkit-scrollbar {
display: none;
}
Ini pola yang sering dipakai di dashboard admin dengan tab navigasi banyak, ketimbang memaksakan wrap yang merusak tinggi navbar.
Untuk grid card, banyak developer langsung lompat ke CSS Grid (akan kita bahas modul berikutnya). Tapi Flexbox tetap pilihan tepat ketika kamu butuh wrapping otomatis dengan lebar item fleksibel tanpa mendefinisikan jumlah kolom secara eksplisit — misalnya daftar tag, produk dengan jumlah tidak tetap, atau grid yang harus "mengalir" secara alami.
.card-grid {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
}
.card {
flex: 1 1 280px; /* basis 280px, boleh tumbuh & menyusut */
min-width: 0; /* cegah overflow konten panjang seperti teks tanpa spasi */
}
flex-basis: 280px berfungsi sebagai lebar "ideal" — browser akan menyesuaikan lebar aktual berdasarkan sisa ruang, sehingga di layar lebar kamu bisa dapat 4 kolom, di tablet 2 kolom, dan di mobile otomatis 1 kolom, tanpa satu pun media query untuk mengatur jumlah kolom. Ini yang disebut intrinsic responsive design — layout menyesuaikan diri berdasarkan sifat elemen, bukan breakpoint kaku.
Tanpa min-width: 0 pada flex item, konten seperti teks panjang tanpa spasi (URL, judul produk) bisa memaksa card melebihi flex-basis-nya dan merusak wrapping. Ini salah satu bug paling sering ditemui saat code review layout flex production.
Card sendiri sering butuh flex internal agar footer (harga, tombol) selalu menempel di bawah meskipun tinggi konten bervariasi:
.card {
display: flex;
flex-direction: column;
flex: 1 1 280px;
}
.card__body {
flex: 1 1 auto; /* mengisi sisa tinggi */
}
.card__footer {
flex: 0 0 auto;
margin-top: auto; /* opsi alternatif tanpa mengubah body */
}
Pola nested flex seperti ini — flex container di luar untuk grid, flex container lagi di dalam untuk struktur card — adalah praktik umum di komponen UI production, dan jauh lebih ringan dibanding mengatur tinggi manual dengan height tetap.
Kamu di-briefing memperbaiki navbar sebuah aplikasi e-commerce dengan masalah berikut yang dilaporkan tim QA:
Langkah penyelesaian yang bisa kamu praktikkan:
flex: 1 atau tidak punya deklarasi flex sama sekali (default flex: 0 1 auto masih bisa menyusut). Ubah keduanya menjadi flex: 0 0 auto, lalu beri text-overflow: ellipsis dan max-width pada brand agar nama panjang terpotong rapi, bukan mendorong elemen lain..navbar__menu seperti contoh di atas, atau — jika desain mengizinkan — pindahkan menu ke dalam elemen collapsible untuk breakpoint di bawah lebar tertentu.flex-basis pada card. Kemungkinan besar nilainya terlalu besar (misal 400px) sehingga di lebar 700px hanya muat satu kolom. Turunkan basis ke sekitar 220–260px, lalu uji ulang di beberapa lebar layar untuk memastikan jumlah kolom terasa proporsional di setiap breakpoint natural.Setelah memperbaiki ketiganya, buka DevTools, ubah lebar viewport secara manual dari 320px hingga 1440px tanpa refresh, dan amati apakah navbar dan card grid berubah secara mulus tanpa "patahan" aneh. Jika sudah mulus tanpa satu pun media query untuk mengatur jumlah kolom card, kamu sudah berhasil menerapkan intrinsic responsive design dengan Flexbox — fondasi yang akan kita bandingkan langsung dengan CSS Grid di modul berikutnya.
Di dua modul sebelumnya kita sudah membangun navbar dan card layout dengan Flexbox — layout satu dimensi yang mengalir sepanjang satu axis. CSS Grid berangkat dari filosofi berbeda: kamu mendesain struktur dua dimensi (baris dan kolom sekaligus) lebih dulu, baru menempatkan konten ke dalamnya. Ini bukan soal "Grid lebih canggih dari Flexbox", tapi soal alat yang tepat untuk masalah yang tepat. Modul ini fokus ke tiga hal yang sering bikin developer menengah stuck: grid-template-areas, perbedaan implicit vs explicit grid, dan bagaimana keduanya berinteraksi saat konten dinamis masuk ke layout.
Kebanyakan tutorial dasar mengajarkan Grid lewat grid-template-columns dan grid-column: 1 / 3. Itu valid, tapi di kode produksi dengan banyak developer, pendekatan ini cepat jadi tidak terbaca — kamu harus menghitung angka kolom di kepala. grid-template-areas menyelesaikan masalah ini dengan mendefinisikan layout secara visual, mirip ASCII art.
.dashboard {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 64px 1fr 48px;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
min-height: 100vh;
}
.sidebar { grid-area: sidebar; }
.header { grid-area: header; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Perhatikan: setiap baris string harus punya jumlah kolom yang sama, dan sel yang bersebelahan dengan nama area sama otomatis digabung (spanning). Kalau kamu ingin ada sel kosong, gunakan titik (.) sebagai placeholder.
grid-template-areas:
"logo nav nav"
"logo . cta";
Best practice di dunia kerja: gunakan grid-template-areas untuk layout level halaman/komponen besar (dashboard, page shell, card kompleks), bukan untuk layout kecil berulang seperti list item — di situ Flexbox atau Grid kolom sederhana lebih ringkas.
Grid areas wajib berbentuk rectangular. Definisi seperti ini akan invalid dan diabaikan browser:
/* INVALID - bentuk L, bukan persegi panjang */
grid-template-areas:
"a a b"
"a c c";
Browser akan silently gagal menerapkan area ini tanpa error di console yang jelas — ini jebakan klasik yang bikin developer junior debugging lama. Selalu validasi bentuk area di kepala (atau di DevTools Grid Inspector) sebelum menyalahkan CSS lain.
Explicit grid adalah struktur yang kamu definisikan secara sengaja lewat grid-template-columns/grid-template-rows/grid-template-areas. Implicit grid adalah baris/kolom yang dibuat otomatis oleh browser ketika item melebihi struktur eksplisit yang sudah kamu tentukan.
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* tidak ada grid-template-rows */
}
/* Jika ada 7 item, browser otomatis membuat
3 baris implicit dengan tinggi default (auto) */
Masalah muncul saat kamu butuh kontrol atas tinggi baris implicit ini — misalnya card grid yang datanya dari API dan jumlah barisnya tidak bisa diprediksi. Di sinilah grid-auto-rows dan grid-auto-columns berperan:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-auto-rows: minmax(160px, auto);
gap: 1rem;
}
Kombinasi auto-fill/auto-fit dengan minmax() ini adalah pola paling sering dipakai untuk grid produk/kartu responsif tanpa media query sama sekali — browser otomatis menghitung berapa kolom yang muat berdasarkan lebar container.
auto-fill: tetap membuat kolom kosong (invisible track) jika ruang tersisa, sehingga item tidak melebar mengisi ruang.auto-fit: kolom kosong "diciutkan" ke 0, sehingga item yang ada melebar mengisi sisa ruang.Kalau kamu punya 3 card dalam container yang cukup untuk 5 kolom: dengan auto-fill, 3 card akan tetap di lebar minmax dan ada ruang kosong di kanan. Dengan auto-fit, 3 card itu melebar mengisi seluruh baris. Pilih auto-fit jika kamu ingin card selalu memenuhi lebar container; pilih auto-fill jika kamu ingin ukuran card konsisten meski jumlahnya sedikit.
Secara default, item mengisi grid baris demi baris (row). Kamu bisa mengubahnya jadi kolom demi kolom, atau mengaktifkan mode dense untuk mengisi celah kosong akibat item dengan ukuran berbeda:
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-flow: row dense;
}
dense berguna untuk galeri gambar dengan ukuran campuran (masonry-like), tapi hati-hati: mode ini bisa mengubah urutan visual item berbeda dari urutan DOM, yang berdampak buruk untuk aksesibilitas (screen reader membaca sesuai urutan DOM, bukan urutan visual). Gunakan hanya jika urutan visual tidak krusial secara semantik.
Di proyek nyata, kamu jarang pakai grid 100% eksplisit atau 100% implisit — biasanya kombinasi. Contoh: header dan sidebar didefinisikan eksplisit lewat grid-template-areas, sementara konten di area main berisi grid card yang jumlahnya dinamis dan dibiarkan implicit.
.main {
grid-area: main;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-auto-rows: minmax(120px, auto);
gap: 1rem;
align-content: start; /* penting: cegah baris meregang penuh saat konten sedikit */
}
Tanpa align-content: start, jika item di area main sedikit, baris-baris akan meregang mengisi seluruh tinggi container (karena default align-content: stretch) — ini sering jadi bug visual "kenapa card-nya jadi tinggi banget" yang membingungkan developer baru.
Skenario: Kamu diminta membangun shell layout untuk dashboard admin internal. Requirement dari tim produk:
.collapsed).Langkah penyelesaian yang bisa kamu coba sendiri:
grid-template-areas seperti contoh header/sidebar/main/footer di atas. Gunakan grid-template-columns: 240px 1fr sebagai default..dashboard.collapsed { grid-template-columns: 72px 1fr; } — perhatikan area tidak perlu didefinisikan ulang, hanya ukuran kolomnya.main, buat grid baru dengan grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)) dan gap: 1rem agar jumlah kolom otomatis menyesuaikan lebar layar tanpa media query.grid-auto-rows: minmax(140px, auto) agar kartu implicit (baris tambahan saat 8 kartu) punya tinggi konsisten dengan baris eksplisit pertama.align-content: start pada container main.Simpan hasil studi kasus ini, karena di modul berikutnya kita akan membandingkan langsung kapan bagian dari layout ini sebaiknya tetap pakai Grid, dan kapan justru lebih tepat dikembalikan ke Flexbox.
Sejauh ini kamu sudah menguasai Flexbox secara mendalam (Modul 2-3) dan Grid Fundamentals (Modul 4). Pertanyaan yang sering muncul di tim frontend production: "ini pakai Grid atau Flexbox?" Jawabannya bukan soal preferensi, tapi soal arah aliran data layout. Modul ini fokus pada pengambilan keputusan tersebut, bukan mengulang sintaks.
Flexbox adalah model satu dimensi — ia sangat baik mengatur elemen dalam satu arah (baris atau kolom) dan mendistribusikan ruang sisa secara dinamis berdasarkan konten. Grid adalah model dua dimensi — ia mengatur baris dan kolom secara bersamaan, cocok saat kamu punya struktur layout yang perlu dikontrol presisi dari dua sumbu sekaligus.
Pertanyaan kunci yang harus kamu tanyakan sebelum menulis kode:
Ini konsep paling praktis untuk membedakan keduanya. Content-out berarti kamu mulai dari item, lalu container menyesuaikan (contoh: daftar tag/badge yang jumlahnya tidak pasti, tombol dalam toolbar). Layout-in berarti kamu mendesain "kerangka" ruang duluan (header-sidebar-main-footer), baru menaruh konten ke dalam slot yang sudah didefinisikan.
/* Content-out: cocok Flexbox */
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Layout-in: cocok Grid */
.page-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 240px 1fr;
}
Di proyek produksi nyata, kamu jarang memilih salah satu secara eksklusif. Pola paling umum: gunakan Grid untuk kerangka halaman besar (macro layout), lalu Flexbox di dalam setiap grid area untuk mengatur elemen internal (micro layout).
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.5rem;
}
.card {
display: flex;
flex-direction: column;
justify-content: space-between; /* footer card selalu di bawah */
}
Perhatikan: grid-template-columns mengatur berapa banyak card yang muat per baris secara otomatis, sementara flex-direction: column di dalam .card memastikan tombol aksi selalu menempel di bawah meski tinggi konten tiap card berbeda-beda. Ini kombinasi yang hampir selalu muncul di dashboard, katalog produk, dan halaman listing.
Kesalahan umum: memaksakan Grid untuk navbar sederhana. Navbar biasanya satu dimensi (logo — menu — tombol aksi dalam satu baris), jadi Flexbox dengan justify-content: space-between jauh lebih ringkas daripada mendefinisikan grid-template-columns yang kaku. Sebaliknya, kesalahan lain: memaksakan Flexbox untuk galeri gambar dengan ukuran seragam yang butuh alignment baris-kolom presisi — di sini nth-child hack dan flex-basis yang rumit sering muncul padahal cukup satu baris grid-template-columns: repeat(4, 1fr).
Indikator lain: jika kamu mulai menulis margin-left: auto berulang kali untuk "mendorong" elemen ke posisi tertentu dalam Flexbox yang kompleks, itu tanda struktur sebenarnya butuh Grid dua dimensi.
Kamu ditugaskan merapikan halaman dashboard admin yang saat ini semua layoutnya pakai Flexbox campur aduk dengan banyak margin manual, sehingga sulit di-maintain dan berantakan saat resize. Strukturnya:
Langkah penyelesaian yang bisa kamu coba sendiri:
grid-template-areas: "sidebar header" "sidebar main"; dengan grid-template-columns: 240px 1fr;. Ini level makro — kamu sudah tahu strukturnya duluan.display: flex; justify-content: space-between; align-items: center; agar judul, search bar, dan avatar tersebar rata dalam satu baris tanpa margin manual.grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; sehingga jumlah kolom otomatis menyesuaikan lebar layar tanpa media query tambahan.display: flex; flex-direction: column; justify-content: space-between; height: 100%; supaya tombol "detail" selalu rata bawah meski panjang label berbeda-beda.auto-fit dan Flexbox sudah menangani redistribusi ruang secara otomatis.Hasil akhirnya: kode lebih pendek, tidak ada margin hack, dan setiap bagian layout punya tanggung jawab yang jelas — Grid untuk kerangka besar dan distribusi kolom, Flexbox untuk alignment isi dalam satu arah. Pola pikir "macro Grid, micro Flexbox" ini akan terus kamu pakai di modul-modul berikutnya, termasuk saat membangun layout Grid yang lebih kompleks dengan nested grid dan subgrid.
Di modul sebelumnya kamu sudah paham kapan memilih Grid vs Flexbox, dan sudah bisa membuat grid template areas untuk layout halaman utuh. Sekarang kita masuk ke level yang sering bikin developer menengah stuck: bagaimana menyusun grid yang bersarang (nested), mengontrol elemen yang jumlahnya dinamis dengan auto-flow, dan menyelaraskan kolom antar level grid dengan subgrid. Ketiga teknik ini krusial saat kamu membangun UI kompleks seperti dashboard, katalog produk, atau komponen card yang isinya tidak seragam.
Nested grid terjadi ketika sebuah grid item juga menjadi grid container untuk anak-anaknya sendiri. Ini berbeda dengan subgrid — nested grid membuat grid track baru yang independen dari parent, tidak otomatis mewarisi kolom/baris induknya.
.dashboard {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1rem;
}
.main-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.main-content di sini adalah grid item dari .dashboard, sekaligus grid container baru untuk card-card di dalamnya. Pola ini sangat umum: layout global pakai grid dua kolom (sidebar + konten), lalu di dalam area konten kamu bikin grid lagi untuk menyusun kartu produk atau widget.
Kapan pakai nested grid? Ketika struktur kolom/baris di level anak benar-benar tidak perlu selaras dengan level induk. Misalnya sidebar punya 1 kolom, tapi area konten butuh 3 kolom kartu — tidak ada alasan keduanya harus align secara grid track.
Masalah umum di dunia kerja: kamu tidak selalu tahu berapa banyak item yang akan dirender (hasil dari API, CMS, atau state user). Di sinilah kombinasi grid-auto-flow, grid-auto-rows, dan grid-auto-columns berperan — mereka mengatur bagaimana item "tambahan" di luar explicit grid ditempatkan.
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-auto-rows: minmax(150px, auto);
grid-auto-flow: row dense;
gap: 1rem;
}
Beberapa poin penting soal auto-flow:
grid-auto-flow: row (default) mengisi item baris demi baris sesuai urutan DOM.grid-auto-flow: column mengisi kolom demi kolom — berguna untuk layout seperti galeri horizontal atau tabel yang dibaca per kolom.dense membuat browser "mengisi lubang" kosong dengan item berikutnya, bukan strict mengikuti urutan DOM. Efek sampingnya: urutan visual bisa berbeda dari urutan DOM, yang bisa merusak urutan baca screen reader. Pakai dense hanya untuk layout dekoratif (masonry-like grid), bukan konten yang urutannya punya makna semantik.auto-fill vs auto-fit juga sering tertukar: auto-fill tetap membuat kolom kosong jika ruang tersisa (kolom "hantu" yang tidak terisi tapi tetap makan track), sedangkan auto-fit akan mengkolaps kolom kosong itu sehingga item yang ada melebar mengisi ruang. Untuk grid kartu yang harus tetap rapi walau itemnya sedikit, auto-fit biasanya pilihan yang lebih aman.
Ini yang paling sering jadi sumber frustrasi sebelum subgrid ada: kamu punya grid dengan beberapa card, dan tiap card punya struktur internal (judul, deskripsi, tombol) yang ingin align rapi secara horizontal — judul semua card sejajar, tombol semua card sejajar — padahal tinggi konten tiap card berbeda-beda.
Sebelum subgrid, solusinya ribet: pakai flexbox dengan trik flex-grow di elemen tengah, atau paksa tinggi manual dengan JavaScript. Dengan subgrid, anak grid bisa langsung mewarisi track baris/kolom dari induknya.
.card-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
gap: 0.5rem;
}
.card__title { grid-row: 1; }
.card__desc { grid-row: 2; }
.card__action { grid-row: 3; }
Dengan grid-template-rows: subgrid, setiap .card tidak membuat baris sendiri, melainkan "meminjam" definisi baris dari .card-list. Hasilnya, baris ke-2 (deskripsi) di semua card akan punya tinggi yang sama secara otomatis, mengikuti card dengan deskripsi terpanjang — tanpa hack tambahan.
Catatan dukungan browser: subgrid sudah didukung di Firefox, Chrome, dan Safari versi modern (per 2024), tapi cek caniuse.com sebelum dipakai di proyek produksi dengan requirement browser lama. Selalu siapkan fallback nested grid biasa untuk browser yang belum support, misalnya lewat feature query:
@supports (grid-template-rows: subgrid) {
.card { grid-template-rows: subgrid; }
}
Di proyek nyata, nested grid, auto-flow, dan subgrid jarang berdiri sendiri — mereka saling melengkapi. Contoh: grid utama pakai auto-fill untuk jumlah kartu yang dinamis, tiap kartu adalah nested grid, dan bagian dalam kartu memakai subgrid agar elemen-elemennya align lintas kartu.
Skenario: kamu diminta membangun grid katalog produk untuk halaman kategori. Requirement dari tim produk:
Langkah penyelesaian yang bisa kamu coba sendiri:
.product-grid dengan grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) agar jumlah kolom menyesuaikan lebar layar tanpa media query tambahan..card--promo { grid-column: span 2; }. Karena requirement melarang mengacak urutan DOM, jangan pakai grid-auto-flow: dense di sini — biarkan browser mengisi sesuai urutan asli walau ada sedikit ruang kosong di beberapa breakpoint..card, buat grid internal 3 baris (gambar, judul, tombol) dengan grid-template-rows: subgrid, lalu definisikan grid-template-rows di level .product-grid sesuai kebutuhan, atau — jika subgrid antar card sejajar secara vertikal sulit karena tiap card berbeda kolom — gunakan pendekatan alternatif: bungkus grup card dalam satu row wrapper yang jadi parent subgrid-nya.Latihan ini menggabungkan tiga konsep modul ini sekaligus dan mencerminkan permintaan yang realistis: fleksibel secara visual, tapi tetap disiplin secara struktur dan aksesibilitas — sesuatu yang sering diabaikan developer junior demi "yang penting tampilannya bagus".
Setelah modul-modul sebelumnya membekali kamu dengan Flexbox dan Grid, sekarang saatnya bicara soal strategi — bukan cuma teknik. Banyak developer menguasai @media tapi tetap menghasilkan layout yang berantakan di device tertentu, karena mereka tidak punya strategi breakpoint yang jelas. Modul ini membahas dua pendekatan utama: mobile-first dan desktop-first, serta bagaimana menentukan breakpoint berdasarkan konten, bukan berdasarkan nama device.
Mobile-first berarti kamu menulis CSS dasar untuk layar kecil, lalu menambah kompleksitas layout seiring lebar layar bertambah lewat min-width. Desktop-first kebalikannya: mulai dari layout lengkap desktop, lalu menyederhanakan lewat max-width.
/* Mobile-first */
.card-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1200px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Desktop-first */
.card-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
@media (max-width: 1199px) {
.card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767px) {
.card-grid { grid-template-columns: 1fr; }
}
Secara visual hasil akhirnya bisa identik, tapi performa dan maintainability berbeda jauh. Mobile-first menghasilkan CSS yang lebih ringan di perangkat mobile karena browser mobile tidak perlu mem-parsing dan meng-override aturan desktop yang kompleks — ia cukup membaca rule dasar. Ini selaras dengan prinsip progressive enhancement: mulai dari pengalaman minimal yang berfungsi, lalu tingkatkan.
Desktop-first lebih masuk akal jika target utama produk memang desktop (misalnya dashboard internal perusahaan yang 95% diakses dari laptop kantor), atau jika kamu melakukan migrasi dari codebase lama yang sudah desktop-first — memaksakan refactor total ke mobile-first bisa makan waktu tanpa nilai bisnis yang sepadan.
min-width dan max-width) sehingga urutan cascade jadi sulit ditebak.font-size atau padding) juga perlu scaling — hasilnya tampilan mobile "aman" tapi terasa sempit dan padat.px untuk width container), membuat elemen overflow di layar kecil sebelum media query sempat override.Breakpoint berdasarkan device populer (320px untuk iPhone SE, 768px untuk iPad, dst) adalah jebakan klasik. Device baru bermunculan terus dengan resolusi berbeda-beda, dan pendekatan ini membuat CSS kamu rapuh terhadap perubahan pasar. Pendekatan yang lebih tahan lama: breakpoint ditentukan oleh titik di mana konten kamu mulai terlihat rusak — bukan oleh lebar device tertentu.
Caranya praktis: resize browser secara manual, perhatikan kapan teks mulai terlalu panjang per baris, kapan card jadi terlalu sempit untuk menampung gambar+teks dengan nyaman, atau kapan navbar mulai bertabrakan. Titik itulah breakpoint kamu — disebut juga content-based breakpoint.
/* Contoh: breakpoint ditentukan saat card mulai kehilangan proporsi ideal */
.product-card {
display: flex;
flex-direction: column;
}
@media (min-width: 540px) {
/* di titik ini card cukup lebar untuk layout horizontal */
.product-card {
flex-direction: row;
}
}
Untuk proyek skala besar, praktik umum adalah menetapkan 3–5 breakpoint standar sebagai "jangkar" tim (misalnya 480px, 768px, 1024px, 1280px) agar konsisten di seluruh komponen, lalu menambahkan breakpoint custom per komponen jika memang dibutuhkan konten spesifik. Jangan membuat breakpoint baru di setiap komponen tanpa alasan — ini menyebabkan CSS membengkak dan sulit di-maintain tim lain.
Satu hal yang sering diabaikan: menulis media query dengan px tidak ikut scaling ketika user mengubah pengaturan zoom text browser (accessibility setting). Menggunakan em pada media query lebih aman karena relatif terhadap font-size default browser, bukan terhadap root elemen halaman seperti rem.
/* px: breakpoint tetap di 768px fisik, tidak terpengaruh pengaturan user */
@media (min-width: 768px) { ... }
/* em: breakpoint ikut membesar jika user memperbesar font default browser */
@media (min-width: 48em) { ... }
Untuk tim yang serius soal aksesibilitas, standar industri saat ini condong memakai em pada media query, sementara rem tetap dipakai untuk spacing dan typography di dalam konten.
Media query punya keterbatasan mendasar: ia hanya tahu lebar viewport, bukan lebar container tempat komponen itu berada. Akibatnya, komponen seperti card yang dipakai di sidebar (sempit) dan di main content (lebar) tidak bisa merespons ukuran kontainernya sendiri — hanya merespons layar. Ini jadi alasan kenapa modul berikutnya membahas Container Queries, pendekatan yang memungkinkan komponen benar-benar "sadar diri" terhadap ruang yang ia tempati, bukan hanya layar perangkat.
Kamu bergabung dengan tim yang mewarisi kode landing page e-commerce. Developer sebelumnya menulis breakpoint berdasarkan device populer dan hasilnya: di lebar layar 900px (laptop kecil / tablet landscape), grid produk menampilkan 4 kolom yang membuat gambar produk terlalu kecil dan teks harga terpotong.
/* Kode warisan — bermasalah */
.products {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (max-width: 1024px) {
.products { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 768px) {
.products { grid-template-columns: repeat(2, 1fr); }
}
Langkah penyelesaian yang bisa kamu coba sendiri:
auto-fit agar tidak perlu breakpoint manual sama sekali (menggabungkan konsep Grid dari modul 4–6 dengan strategi responsif modul ini):.products {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
em, bukan px, dan uji dengan zoom browser 150–200%.Hasil akhirnya: grid produk otomatis menyesuaikan jumlah kolom berdasarkan ruang yang tersedia tanpa breakpoint device-specific yang rapuh, dan navbar tetap konsisten terhadap pengaturan aksesibilitas user. Coba terapkan pola auto-fit + minmax ini pada komponen grid lain di proyekmu, lalu bandingkan berapa baris media query yang berhasil kamu hilangkan.
Di modul 7 kita sudah membahas breakpoint berdasarkan lebar viewport (mobile-first vs desktop-first). Pendekatan itu bekerja baik untuk layout halaman secara keseluruhan, tapi punya kelemahan mendasar: breakpoint viewport tidak tahu apa-apa tentang konteks tempat komponen itu dipasang. Sebuah card yang sama bisa diletakkan di sidebar sempit atau di area konten utama yang lebar — padahal viewport-nya identik. Akibatnya, developer sering menulis media query yang targetnya sebenarnya bukan ukuran layar, tapi ukuran ruang yang tersedia untuk komponen tersebut.
Inilah masalah yang coba diselesaikan Container Queries: media query versi "lokal" yang bereaksi terhadap ukuran parent container, bukan ukuran viewport browser. Komponen jadi benar-benar reusable di konteks apa pun — inilah alasan kenapa pola ini disebut pendekatan responsif generasi baru, dan kenapa penting dikuasai sebelum kita masuk ke proyek akhir yang menggabungkan komponen-komponen kompleks.
Agar sebuah elemen bisa "diamati" ukurannya oleh anak-anaknya, elemen tersebut harus dideklarasikan sebagai containment context lewat properti container-type.
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
padding: 1rem;
display: flex;
flex-direction: column;
}
@container card (min-width: 400px) {
.card {
flex-direction: row;
gap: 1.5rem;
}
}
Beberapa poin krusial yang sering salah dipahami:
container-type: inline-size berarti kita hanya mengukur lebar (arah inline). Ini pilihan paling umum karena mengukur tinggi (size) bisa menyebabkan circular dependency pada layout block.container-name bersifat opsional, tapi sangat disarankan di proyek produksi karena satu halaman bisa punya banyak container bertingkat — tanpa nama, @container query akan menarget container terdekat secara default, yang gampang salah sasaran saat nesting dalam.contain: layout style secara implisit, sehingga perubahan ukuran di dalamnya tidak memicu reflow ke luar — ini juga manfaat performa tambahan.cqw, cqh, cqi, cqb bisa dipakai untuk membuat nilai yang relatif terhadap ukuran container, bukan viewport — berguna untuk font-size atau spacing yang proporsional terhadap container.Container query bukan pengganti media query, melainkan pelengkap. Gunakan media query untuk keputusan struktural level halaman (misalnya mengubah jumlah kolom grid utama), dan container query untuk keputusan level komponen (misalnya mengubah arah flex di dalam satu card). Mencampuradukkan keduanya tanpa strategi jelas membuat kode sulit ditelusuri saat debugging — developer baru di tim akan bingung breakpoint mana yang bertanggung jawab atas perubahan apa.
Pendekatan tradisional mengatur ukuran font berbeda di tiap breakpoint menghasilkan transisi "patah" — teks melompat dari satu ukuran ke ukuran lain tepat di titik breakpoint. Fluid typography menyelesaikan ini dengan fungsi clamp(), yang menghitung nilai minimum, nilai preferensi (biasanya berbasis vw), dan nilai maksimum.
h1 {
font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
}
Cara membaca rumus ini: nilai akan mengikuti 1.2rem + 2.5vw secara linear selama hasilnya berada di antara 1.75rem dan 3rem. Di luar rentang itu, nilai dikunci ke batas minimum atau maksimum. Berbeda dengan media query bertingkat yang butuh banyak breakpoint untuk hasil halus, satu baris clamp() sudah menghasilkan skala yang mulus di semua ukuran layar.
Praktik produksi yang disarankan:
vw secara manual lewat trial-error. Gunakan rumus interpolasi: slope = (max - min) / (maxViewport - minViewport), lalu preferred = slope * 100vw + intercept. Banyak generator online melakukan ini otomatis, tapi penting memahami rumusnya agar bisa menyesuaikan manual saat butuh.clamp() dengan custom property dari modul 1, misalnya --fs-heading: clamp(...), supaya skala tipografi konsisten dipakai ulang di seluruh sistem desain.clamp() lebih ketat (selisih min-max kecil) karena pembaca sensitif terhadap perubahan ukuran teks panjang; untuk heading besar, rentang boleh lebih lebar karena perannya dekoratif.Kombinasi keduanya sangat kuat: gunakan unit cqi di dalam clamp() agar ukuran teks sebuah komponen menyesuaikan lebar container tempatnya dipasang, bukan viewport.
.card-title {
font-size: clamp(1rem, 0.8rem + 2cqi, 1.5rem);
}
Dengan ini, judul card akan otomatis mengecil saat card dipasang di sidebar sempit, dan membesar saat dipasang di grid utama yang lebar — tanpa satu baris media query pun.
Container queries sudah didukung luas di browser modern (Chrome, Edge, Firefox, Safari versi terbaru), tapi tetap sediakan fallback untuk browser lama menggunakan @supports:
.card {
flex-direction: column;
}
@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}
}
Strategi ini memastikan browser yang tidak mendukung container query tetap mendapat layout dasar yang berfungsi (progressive enhancement), bukan layout yang rusak.
Bayangkan kamu bekerja di tim frontend sebuah platform e-commerce. Desainer memberi satu komponen ProductCard yang harus dipakai di tiga tempat: (1) grid katalog utama dengan lebar kolom besar, (2) widget "produk serupa" di sidebar sempit, dan (3) carousel horizontal di halaman checkout dengan lebar kartu sedang. Tim desain hanya memberi satu set style guide, bukan tiga versi komponen terpisah — dan kamu dilarang menulis class modifier seperti .product-card--small karena akan membengkak saat varian konteks bertambah di masa depan.
Langkah penyelesaian yang bisa kamu coba sendiri:
ProductCard dengan wrapper yang diberi container-type: inline-size dan container-name: product-card..product-card: gambar di atas, judul dan harga di bawah, tersusun vertikal (flex-direction: column), ini adalah tampilan default saat container sempit.@container product-card (min-width: 280px) yang mengubah layout menjadi horizontal (gambar di kiri, teks di kanan) — cocok untuk konteks katalog utama yang lebar.clamp() berbasis cqi, sehingga teks menyesuaikan proporsional di sidebar sempit maupun grid lebar tanpa aturan tambahan.@supports dengan layout vertikal sebagai default aman untuk browser yang belum mendukung container query.Hasil akhirnya: satu komponen, nol modifier class, adaptif terhadap konteks pemasangan apa pun. Inilah standar yang akan kamu pakai lagi di modul Proyek Akhir saat menyusun dashboard dengan banyak widget yang ditempatkan di area berbeda-beda.
Modul baru ditambahkan secara berkala hingga kelas ini lengkap. Simpan halaman ini dan kembali lagi untuk lanjut belajar.