1. Pengenalan Container Queries
CSS Container Queries adalah salah satu fitur CSS yang paling dinanti selama bertahun-tahun. Fitur ini memungkinkan sebuah elemen merespons ukuran atau style dari parent container-nya, bukan dari ukuran viewport (layar) seperti yang dilakukan @media queries.
Ini mengubah paradigma responsive design secara fundamental. Sebelumnya, komponen hanya bisa merespons ukuran layar. Sekarang, komponen bisa merespons ukuran di mana ia ditempatkan. Artinya, satu komponen bisa tampil berbeda tergantung apakah ia berada di sidebar yang sempit atau main content yang lebar β tanpa perlu JavaScript.
Media Queries vs Container Queries
| Aspek | @media Queries | @container Queries |
|---|---|---|
| Target | Ukuran viewport (layar) | Ukuran parent container |
| Konteks | Global (seluruh halaman) | Lokal (komponen tertentu) |
| Reusability | Komponen terikat layout halaman | Komponen benar-benar mandiri |
| Kapan cocok | Layout halaman, sidebar, grid | Komponen reusable (card, widget) |
| Dukungan | βββ Universal | βββ Semua browser modern (2023+) |
Container Queries bukan pengganti Media Queries β keduanya saling melengkapi. Gunakan Media Queries untuk layout halaman dan Container Queries untuk komponen yang perlu fleksibel di berbagai kontainer.
Dukungan Browser
CSS Container Queries didukung oleh semua browser modern sejak Maret 2023:
- β Chrome 105+ (Agustus 2022)
- β Edge 105+ (Agustus 2022)
- β Safari 16+ (September 2022)
- β Firefox 110+ (Februari 2023)
- β Opera 91+
Coverage global saat ini sudah di atas 92% β aman digunakan di production.
2. Masalah yang Dipecahkan
Untuk memahami mengapa Container Queries begitu penting, mari kita lihat masalah yang sering dihadapi developer:
Problem: Komponen yang Tidak Fleksibel
/* Card component dengan media queries */
.card {
display: flex;
flex-direction: column;
}
/* Di layar lebar, tampil horizontal */
@media (min-width: 768px) {
.card {
flex-direction: row;
}
.card img {
width: 200px;
height: 200px;
}
}
/* MASALAH: Bagaimana jika card ini dipakai di sidebar? */
/* Sidebar mungkin hanya 300px lebarnya */
/* Tapi card tetap tampil horizontal karena viewport > 768px */
/* Hasil: card terlihat jelek dan terlalu sempit di sidebar */
Solusi: Container Queries
/* Container: parent yang menentukan */
.card-wrapper {
container-type: inline-size;
}
/* Card merespons ukuran CARD-WRAPPER, bukan viewport */
.card {
display: flex;
flex-direction: column;
}
@container (min-width: 500px) {
.card {
flex-direction: row;
}
.card img {
width: 200px;
height: 200px;
}
}
/* Sekarang card otomatis adaptif: */
/* - Di main content (lebar): tampil horizontal */
/* - Di sidebar (sempit): tampil vertical */
/* - TANPA perlu JavaScript atau class tambahan */
Skema Konsep
ββββββββββββββββββββββββββββββββββββββββββββββββββββ β Viewport (Browser Window) β β β β βββββββββββββββββββββββ ββββββββββββββββββ β β β Main Content β β Sidebar β β β β β β β β β β ββββββββββββββββ β β ββββββββββββ β β β β β Card Wrapper β β β βCard Wrap β β β β β β (800px wide) β β β β(300px) β β β β β β β β β β β β β β β β Card tampil β β β βCard tampilβ β β β β β HORIZONTAL β β β βVERTICAL β β β β β β (container β β β β(containerβ β β β β β > 500px) β β β β< 500px) β β β β β ββββββββββββββββ β β ββββββββββββ β β β β β β β β β βββββββββββββββββββββββ ββββββββββββββββββ β β β β β Container query: responsif terhadap ukuran β β container, BUKAN viewport β ββββββββββββββββββββββββββββββββββββββββββββββββββββ
3. CSS Containment
Sebelum menggunakan Container Queries, kita perlu memahami konsep CSS Containment. Container Queries bekerja dengan memanfaatkan containment β mekanisme yang memberitahu browser bahwa isi suatu elemen independen dari elemen lain di halaman.
container-type Property
/* inline-size containment: container queries inline (horizontal) size */
.card-wrapper {
container-type: inline-size;
}
/* size containment: queries BOTH inline AND block size */
.grid-cell {
container-type: size;
}
/* normal: tidak menjadi container (default) */
.normal-element {
container-type: normal;
}
| container-type | Containment | Bisa Query |
|---|---|---|
inline-size |
Layout inline containment | width / inline-size |
size |
Layout size containment | width & height (inline & block) |
normal |
Tidak ada containment | Hanya style queries |
container-name
/* Memberi nama pada container */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Shorthand: container: name type */
.card-wrapper {
container: card / inline-size;
}
/* Multiple names */
.widget-wrapper {
container: widget responsive / inline-size;
}
/* Query berdasarkan nama */
@container card (min-width: 500px) {
.card {
flex-direction: row;
}
}
/* Query tanpa nama (match container terdekat) */
@container (min-width: 500px) {
.card {
flex-direction: row;
}
}
/* Query dengan nama spesifik */
@container card (min-width: 500px) {
.card-title {
font-size: 1.5rem;
}
}
/* Query dengan nama berbeda */
@container sidebar (max-width: 300px) {
.nav-item {
display: block;
}
}
Elemen dengan container-type: inline-size atau size tidak bisa menggunakan height: auto atau aspect-ratio yang berisi konten. Containment membutuhkan ukuran yang terdefinisi. Gunakan min-height jika diperlukan.
4. @container Rule
Aturan @container bekerja mirip dengan @media, tetapi menspecifikasi kondisi berdasarkan ukuran container terdekat yang memiliki containment, bukan viewport.
Syntax Dasar
/* Tanpa nama: match container terdekat */
@container (min-width: 400px) {
.card-title {
font-size: 1.25rem;
}
}
/* Dengan nama: match container dengan nama tertentu */
@container card (min-width: 400px) {
.card-title {
font-size: 1.25rem;
}
}
/* Negation: not */
@container card not (min-width: 400px) {
.card-title {
font-size: 1rem;
}
}
/* Multiple conditions: AND */
@container card (min-width: 400px) and (max-width: 800px) {
.card {
padding: 1rem;
}
}
/* Multiple conditions: OR */
@container card (min-width: 800px) or (orientation: landscape) {
.card {
flex-direction: row;
}
}
Container Features yang Bisa di-Query
| Feature | Deskripsi | Contoh |
|---|---|---|
width |
Lebar container | (min-width: 500px) |
height |
Tinggi container | (min-height: 300px) |
inline-size |
Lebar inline axis | (min-inline-size: 500px) |
block-size |
Tinggi block axis | (min-block-size: 300px) |
aspect-ratio |
Rasio lebar:tinggi | (aspect-ratio > 16/9) |
orientation |
Portrait atau landscape | (orientation: landscape) |
Breakpoint Pattern
/* Mobile-first container breakpoints */
/* Default: tampilan paling kecil */
.card {
display: flex;
flex-direction: column;
padding: 1rem;
gap: 0.5rem;
}
/* Small: container cukup lebar */
@container (min-width: 350px) {
.card {
padding: 1.5rem;
}
.card-title {
font-size: 1.25rem;
}
}
/* Medium: container lebar */
@container (min-width: 550px) {
.card {
flex-direction: row;
gap: 1.5rem;
}
.card-image {
width: 200px;
flex-shrink: 0;
}
}
/* Large: container sangat lebar */
@container (min-width: 750px) {
.card {
gap: 2rem;
padding: 2rem;
}
.card-image {
width: 300px;
}
.card-title {
font-size: 1.5rem;
}
.card-meta {
display: flex;
gap: 1rem;
}
}
5. Size Queries
Size queries adalah jenis container query yang paling umum digunakan. Mereka memungkinkan elemen merespons dimensi (lebar, tinggi) dari container-nya.
Responsive Card Component
<!-- Container wrapper -->
<div class="card-container">
<article class="card">
<img class="card-image" src="photo.jpg" alt="Product">
<div class="card-body">
<h3 class="card-title">Product Name</h3>
<p class="card-desc">Deskripsi produk yang panjang...</p>
<div class="card-meta">
<span class="card-price">Rp 150.000</span>
<button class="card-btn">Beli</button>
</div>
</div>
</article>
</div>
/* Container declaration */
.card-container {
container: card / inline-size;
}
/* ===== DEFAULT (Small): < 350px ===== */
.card {
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-body {
padding: 1rem;
}
.card-title {
font-size: 1.125rem;
margin-bottom: 0.5rem;
}
.card-desc {
font-size: 0.875rem;
color: #666;
margin-bottom: 1rem;
display: none; /* Terlalu kecil, sembunyikan */
}
.card-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-price {
font-size: 1rem;
font-weight: 700;
}
.card-btn {
padding: 0.5rem 1rem;
font-size: 0.75rem;
}
/* ===== MEDIUM: >= 350px ===== */
@container card (min-width: 350px) {
.card-title {
font-size: 1.25rem;
}
.card-desc {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-btn {
padding: 0.625rem 1.5rem;
font-size: 0.875rem;
}
}
/* ===== LARGE: >= 600px ===== */
@container card (min-width: 600px) {
.card {
flex-direction: row;
}
.card-image {
width: 250px;
height: auto;
flex-shrink: 0;
}
.card-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.card-title {
font-size: 1.5rem;
}
.card-desc {
display: block;
-webkit-line-clamp: unset;
}
}
/* ===== EXTRA LARGE: >= 800px ===== */
@container card (min-width: 800px) {
.card-image {
width: 350px;
}
.card-body {
padding: 2rem;
}
.card-title {
font-size: 1.75rem;
}
.card-meta {
flex-wrap: wrap;
gap: 1rem;
}
}
Height-Based Queries
.sidebar-container {
container: sidebar / size; /* Perlu 'size' untuk height query */
}
.sidebar {
display: flex;
flex-direction: column;
}
/* Jika sidebar cukup tinggi, tampilkan label */
@container sidebar (min-height: 400px) {
.sidebar-label {
display: block;
}
}
/* Jika sidebar sangat tinggi, tampilkan footer */
@container sidebar (min-height: 600px) {
.sidebar-footer {
display: block;
margin-top: auto;
}
}
/* Orientation query */
@container sidebar (orientation: landscape) {
.sidebar {
flex-direction: row;
}
}
6. Style Queries
Style queries memungkinkan elemen merespons style (custom properties / CSS variables) dari container-nya. Ini membuka kemungkinan yang sangat powerful untuk komponen theming dan conditional styling.
Custom Property Style Queries
/* Container dengan custom property */
.card-container {
container: card / inline-size;
--theme: light;
--featured: false;
}
/* Dark theme container */
.card-container.dark {
--theme: dark;
}
/* Featured container */
.card-container.featured {
--featured: true;
}
/* Style query: respond to custom properties */
@container card style(--theme: dark) {
.card {
background: #1a1a2e;
color: #e0e0e0;
border-color: #333;
}
.card-title {
color: #ffffff;
}
}
@container card style(--theme: light) {
.card {
background: #ffffff;
color: #333333;
border-color: #e0e0e0;
}
}
@container card style(--featured: true) {
.card {
border: 2px solid gold;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.3);
}
.card-title {
color: #b8860b;
}
.card-badge {
display: block;
}
}
Boolean Style Queries
/* Boolean: apakah custom property defined? */
.navigation {
--icons-only: true;
}
@container style(--icons-only) {
.nav-text {
display: none;
}
.nav-icon {
font-size: 1.5rem;
}
}
/* Sidebar collapsed state */
.sidebar {
--collapsed: true;
}
@container style(--collapsed) {
.sidebar-text {
display: none;
}
.sidebar {
width: 60px;
}
}
Theme Switching dengan Style Queries
/* Universal theme container */
.theme-provider {
--theme: light;
--color-primary: #3498db;
--color-bg: #ffffff;
--color-text: #333333;
--color-border: #e0e0e0;
}
.theme-provider[data-theme="dark"] {
--theme: dark;
--color-primary: #5dade2;
--color-bg: #1a1a2e;
--color-text: #ecf0f1;
--color-border: #34495e;
}
/* Component responds to theme via style queries */
@container style(--theme: dark) {
.card {
background: var(--color-bg);
color: var(--color-text);
border-color: var(--color-border);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.input-field {
background: #2c2c3e;
border-color: #444;
color: white;
}
.input-field::placeholder {
color: #888;
}
}
@container style(--theme: light) {
.card {
background: var(--color-bg);
color: var(--color-text);
border-color: var(--color-border);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
}
Style queries saat ini hanya mendukung custom properties (CSS variables). Browser belum mendukung style queries untuk properties biasa seperti color atau font-size. Gunakan custom property --theme atau --variant sebagai proxy.
7. Contoh Praktis
Responsive Navigation
.nav-container {
container: nav / inline-size;
}
.navbar {
display: flex;
align-items: center;
padding: 1rem;
background: #2c3e50;
}
/* Default: hamburger menu */
.nav-links {
display: none;
}
.nav-toggle {
display: block;
color: white;
font-size: 1.5rem;
cursor: pointer;
}
/* Cukup lebar: tampilkan links horizontal */
@container nav (min-width: 600px) {
.nav-toggle {
display: none;
}
.nav-links {
display: flex;
list-style: none;
gap: 1.5rem;
margin: 0;
padding: 0;
}
.nav-links a {
color: white;
text-decoration: none;
}
}
/* Sangat lebar: tampilkan search bar inline */
@container nav (min-width: 900px) {
.nav-search {
display: flex;
margin-left: auto;
}
.nav-search input {
width: 250px;
padding: 0.5rem;
border-radius: 4px;
}
}
Responsive Grid Item
.grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.grid-item-container {
container: grid-item / inline-size;
}
.grid-item {
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Compact view saat grid item kecil */
@container grid-item (max-width: 300px) {
.grid-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
}
.grid-item-avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
}
.grid-item-details {
display: none;
}
.grid-item-title {
font-size: 0.875rem;
}
}
/* Expanded view saat grid item besar */
@container grid-item (min-width: 400px) {
.grid-item {
padding: 1.5rem;
}
.grid-item-avatar {
width: 80px;
height: 80px;
}
.grid-item-title {
font-size: 1.25rem;
}
.grid-item-details {
display: block;
margin-top: 0.75rem;
font-size: 0.875rem;
color: #666;
}
}
Widget Dashboard
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
padding: 1.5rem;
}
.widget-container {
container: widget / inline-size;
}
.widget {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.widget-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.widget-body {
padding: 1.5rem;
}
/* Compact widget */
@container widget (max-width: 320px) {
.widget-chart {
height: 100px;
}
.widget-stats {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.widget-stat-item {
flex: 1;
min-width: 80px;
text-align: center;
padding: 0.5rem;
}
.widget-stat-value {
font-size: 1.25rem;
}
.widget-stat-label {
font-size: 0.75rem;
display: block;
}
}
/* Full widget */
@container widget (min-width: 450px) {
.widget-chart {
height: 250px;
}
.widget-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
padding: 1rem 0;
}
.widget-stat-item {
text-align: center;
}
.widget-stat-value {
font-size: 2rem;
}
.widget-stat-label {
font-size: 0.875rem;
}
.widget-footer {
display: flex;
justify-content: space-between;
padding: 1rem 1.5rem;
border-top: 1px solid #eee;
}
}
8. Container Units
CSS Container Queries juga memperkenalkan satuan unit baru yang relatif terhadap ukuran container:
| Unit | Penjelasan | Contoh |
|---|---|---|
cqw |
1% dari container width | width: 50cqw; |
cqh |
1% dari container height | height: 30cqh; |
cqi |
1% dari container inline size | font-size: 5cqi; |
cqb |
1% dari container block size | padding: 2cqb; |
cqmin |
Smaller dari cqi atau cqb | width: 80cqmin; |
cqmax |
Larger dari cqi atau cqb | font-size: 5cqmax; |
Fluid Typography dengan Container Units
.card-container {
container: card / inline-size;
}
.card-title {
/* Font size otomatis skala dengan container width */
font-size: clamp(1rem, 5cqi, 2rem);
}
.card-subtitle {
font-size: clamp(0.875rem, 3.5cqi, 1.25rem);
}
.card-body {
font-size: clamp(0.875rem, 2.5cqi, 1rem);
line-height: 1.6;
}
.card-image {
/* Image height responsif terhadap container */
height: 40cqw;
min-height: 150px;
max-height: 300px;
object-fit: cover;
}
.card-padding {
/* Padding adaptif */
padding: 3cqi;
}
/* Badge ukuran responsif */
.card-badge {
font-size: clamp(0.625rem, 2cqi, 0.75rem);
padding: 1cqi 2cqi;
border-radius: 2cqi;
}
Container Units vs Viewport Units
/* Viewport units: berdasarkan browser window */
.hero-title {
font-size: 5vw; /* 5% dari viewport width */
}
/* Container units: berdasarkan container */
.card-title {
font-size: 5cqi; /* 5% dari container inline size */
}
/* Perbedaan terlihat saat komponen di-reuse: */
/* - Hero title: 5vw = selalu sama di mana pun di halaman */
/* - Card title: 5cqi = berbeda tergantung card container */
/* Best practice: gunakan container units untuk komponen, viewport units untuk layout */
9. Best Practices
Kapan Menggunakan Apa
| Situasi | Gunakan | Alasan |
|---|---|---|
| Layout halaman | @media | Layout respons terhadap viewport |
| Komponen reusable | @container | Komponen bisa di mana saja |
| Grid items | @container | Item berbeda ukuran di grid |
| Theme switching | @container style() | Komponen adaptif terhadap tema |
| Typography fluid | container units | Font scale dengan parent |
| Sidebar vs main | @container | Konten di area berbeda |
Tips & Trik
- π― Nama container yang jelas β Gunakan nama yang deskriptif:
card,sidebar,widget - π Default ke compact β Mulai dari tampilan paling kecil, tambah elemen di breakpoint lebih besar
- ποΈ Container di wrapper β Tempatkan
container-typedi wrapper, bukan di komponen itu sendiri - π Combine dengan @media β Gunakan media queries untuk layout, container queries untuk komponen
- π± Mobile-first β Sama seperti media queries, gunakan
min-widthbukanmax-width - π¨ Style queries untuk theming β Lebih fleksibel dari class-based theming
- π cqmin/cqmax β Berguna untuk membatasi scaling di container yang sangat lebar/sempit
Hindari membuat terlalu banyak container di satu halaman. Setiap container membutuhkan browser untuk menghitung layout secara independen, yang bisa berdampak pada performa jika terlalu banyak nesting container.
Fallback Strategy
/* Fallback untuk browser lama */
.card {
/* Default: tampilan mobile (selalu berfungsi) */
flex-direction: column;
}
/* Media query fallback */
@media (min-width: 768px) {
.card {
flex-direction: row;
}
}
/* Container query: override untuk browser yang support */
@supports (container-type: inline-size) {
.card-container {
container: card / inline-size;
}
@container card (min-width: 500px) {
.card {
flex-direction: row;
}
}
/* Reset media query fallback */
@media (min-width: 768px) {
.card {
flex-direction: column; /* Kembali ke default */
}
}
}
10. Quiz Pemahaman
Uji pemahamanmu tentang CSS Container Queries: