Container Queries — Responsive design không phụ thuộc viewport
Bạn có một <ProductCard> component. Trên trang listing nó nằm trong grid 3 cột — card ngang, ảnh bên trái, text bên phải. Trên sidebar "Sản phẩm liên quan" cùng trang — card dọc, ảnh trên, text dưới. Cùng component, khác layout, tuỳ vào container chứa nó.
Media queries không giải quyết được vì viewport width giống nhau cho cả hai. Bạn phải tạo 2 variant, truyền prop layout="horizontal" vs layout="vertical", rồi viết logic CSS riêng. Nhân lên cho mọi component responsive trong app — complexity bùng nổ.
Container Queries giải quyết gọn: component tự biết container nó rộng bao nhiêu, và tự adapt.
Cú pháp cơ bản
/* Bước 1: Đánh dấu container */
.product-grid {
container-type: inline-size;
container-name: product-area;
}
/* Bước 2: Query container thay vì viewport */
@container product-area (min-width: 500px) {
.product-card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
}
@container product-area (max-width: 499px) {
.product-card {
display: flex;
flex-direction: column;
}
}Khi .product-grid rộng ≥500px → card ngang. Khi hẹp hơn → card dọc. Không cần biết viewport bao nhiêu pixel.
container-type giải thích
.wrapper {
/* inline-size: query theo chiều ngang (phổ biến nhất) */
container-type: inline-size;
/* size: query theo cả ngang lẫn dọc */
container-type: size;
/* normal: không phải container (mặc định) */
container-type: normal;
}⚠️
container-type: inline-sizetạo containment trên element. Nghĩa là element không còn phụ thuộc vào content bên trong để xác định kích thước inline — nó phải có explicit width hoặc lấy width từ parent/grid/flex context. Nếu bạn setcontainer-typelên element không có width rõ ràng, nó có thể collapse.
So sánh với media queries
/* Media query: viewport-centric */
@media (min-width: 768px) {
.product-card {
flex-direction: row;
}
}
/* Container query: component-centric */
@container (min-width: 500px) {
.product-card {
flex-direction: row;
}
}Khác biệt quan trọng:
| Tiêu chí | Media Queries | Container Queries |
|---|---|---|
| Dựa trên | Viewport size | Container size |
| Component reusable? | Khó — phụ thuộc context | Dễ — self-contained |
| Nested layouts | Phải tính toán breakpoint thủ công | Tự adapt |
| Browser support | Mọi nơi | Chrome 105+, Safari 16+, Firefox 110+ |
Use case thực tế: Dashboard layout
Dashboard là nơi Container Queries toả sáng. Cùng một widget có thể nằm trong:
- Main area full-width (1200px)
- Side panel (350px)
- Modal (500px)
- Mobile full-screen (375px)
/* Widget container */
.dashboard-cell {
container-type: inline-size;
}
/* Widget tự adapt */
.stats-widget {
display: grid;
gap: 0.75rem;
}
/* Compact: chỉ hiện số chính */
@container (max-width: 250px) {
.stats-widget {
text-align: center;
}
.stats-widget .chart {
display: none;
}
.stats-widget .label {
font-size: 0.75rem;
}
.stats-widget .value {
font-size: 1.5rem;
}
}
/* Medium: số + sparkline */
@container (min-width: 251px) and (max-width: 500px) {
.stats-widget {
grid-template-columns: 1fr auto;
align-items: center;
}
.stats-widget .chart {
width: 80px;
height: 40px;
}
}
/* Wide: full chart + breakdown table */
@container (min-width: 501px) {
.stats-widget {
grid-template-columns: 1fr;
}
.stats-widget .chart {
width: 100%;
height: 200px;
}
.stats-widget .breakdown {
display: table;
}
}Một component, 3 layouts, zero JavaScript. Kéo widget từ main area sang sidebar — CSS tự handle.
Container Query Units
Ngoài breakpoints, bạn có thể dùng container query units cho responsive sizing:
.dashboard-cell {
container-type: inline-size;
}
.widget-title {
/* 5% chiều rộng container, clamp giữa 14px và 24px */
font-size: clamp(0.875rem, 5cqi, 1.5rem);
}
.widget-chart {
/* 50% chiều cao container */
height: 50cqb;
}Các units:
cqi— 1% inline size của containercqb— 1% block size của containercqw— 1% width (alias cho cqi trong horizontal writing mode)cqh— 1% heightcqmin— min(cqi, cqb)cqmax— max(cqi, cqb)
💡
cqikết hợp vớiclamp()rất mạnh. Font size, padding, gap — tất cả scale theo container thay vì viewport. Component trông proportional ở mọi kích thước.
Nested containers
Container Queries tự động resolve container gần nhất:
.page-layout {
container-type: inline-size;
container-name: page;
}
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
/* Query container cụ thể bằng tên */
@container page (min-width: 1024px) {
.sidebar {
width: 300px;
}
}
/* Query container gần nhất (sidebar) */
@container sidebar (max-width: 300px) {
.nav-item {
/* Chỉ hiện icon, ẩn text */
padding: 0.5rem;
}
.nav-item span {
display: none;
}
}Nếu không chỉ định tên, @container query container ancestor gần nhất có container-type khác normal. Khi có nhiều nested containers, nên đặt tên rõ ràng để tránh nhầm.
Style queries — query giá trị CSS custom property
Ngoài size queries, Container Queries còn hỗ trợ style queries — kiểm tra giá trị CSS variable của container:
.theme-area {
container-name: theme;
}
.dark-section {
--theme: dark;
}
.light-section {
--theme: light;
}
/* Style query: check custom property */
@container theme style(--theme: dark) {
.card {
background: #1a1a2e;
color: #eee;
border-color: #333;
}
.card .badge {
background: #e94560;
}
}
@container theme style(--theme: light) {
.card {
background: #fff;
color: #333;
border-color: #ddd;
}
}Use case: dark/light sections trên cùng 1 trang mà không cần class-based theming cho từng element con.
⚠️ Style queries hiện chỉ support custom properties (
--*). Query standard properties nhưstyle(background: red)chưa được implement. Chrome 111+ support, Safari và Firefox đang triển khai.
Pattern: Component tự responsive hoàn toàn
Kết hợp tất cả lại — một card component không cần prop, không cần utility class, tự adapt mọi nơi:
/* Card component — zero external dependencies */
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
gap: 1rem;
padding: 1rem;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
/* Stacked layout: nhỏ */
@container (max-width: 350px) {
.card {
grid-template-columns: 1fr;
}
.card img {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
border-radius: 6px;
}
.card .actions {
flex-direction: column;
}
}
/* Horizontal layout: vừa */
@container (min-width: 351px) and (max-width: 700px) {
.card {
grid-template-columns: 150px 1fr;
align-items: start;
}
.card img {
aspect-ratio: 1;
border-radius: 6px;
}
}
/* Full layout: rộng */
@container (min-width: 701px) {
.card {
grid-template-columns: 250px 1fr auto;
align-items: center;
}
.card .actions {
flex-direction: column;
gap: 0.5rem;
}
}Drop component này vào sidebar 300px → stacked. Vào content area 600px → horizontal. Vào hero section 1000px → full layout. Không cần biết viewport, không cần prop, không cần JavaScript.
Khi nào dùng Container Queries vs Media Queries
Container Queries:
- Reusable components cần adapt theo parent
- Dashboard widgets, card grids, form layouts
- Design system components
- Bất cứ khi nào bạn muốn viết: "if this component's container is X wide"
Media Queries (vẫn cần):
- Page-level layout (sidebar show/hide, navigation collapse)
- Print styles
- Orientation changes
- Accessibility preferences (
prefers-reduced-motion,prefers-color-scheme)
Thực tế hai cái bổ sung nhau. Media queries cho page layout, Container Queries cho component layout. Kết hợp cả hai cho responsive design thực sự component-driven.
Container Queries là thay đổi lớn nhất trong CSS responsive design kể từ khi media queries ra đời. Nó giải quyết vấn đề mà cộng đồng frontend than phiền hơn chục năm: component không thể tự responsive vì bị gắn cứng vào viewport. Giờ thì có thể.