jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

CSS Container Queries — Component-Local Responsive Design Beyond Viewport Media Queries

Deep dive into @container, container-type, cqi units, style queries, containment costs, and the gotchas senior engineers hit when replacing media queries.

Media query trả lời một câu: viewport rộng bao nhiêu? Container query trả lời câu khác: component thực sự chiếm slot rộng bao nhiêu? Thay đổi đó — từ breakpoint cấp trang sang breakpoint cấp component — là thay đổi lớn nhất trong responsive CSS kể từ khi flexbox phổ biến. Bài viết này là bản đồ cho senior engineer: khi nào dùng @container, container-typecontainer-name hoạt động thế nào, container query units, style queries 2025–2026, và các gotcha containment cắn trong production.


Table of contents

  1. Why viewport media queries fail components
  2. The container query model
  3. container-type: inline-size, size, normal
  4. container-name and named @container rules
  5. Container query units (cqw, cqi, cqh, …)
  6. Style queries — @container style(...) in 2025–2026
  7. Gotchas, containment side effects, and debugging
  8. Real component: product card in a dashboard grid
  9. Live demo
  10. Decision checklist

1. Why viewport media queries fail components

Hãy xem <ProductCard> tái sử dụng được đặt ở ba chỗ trên cùng một trang:

  • sidebar 240px
  • rail “related items” 360px
  • cột grid main fluid có thể 600px hoặc 900px tùy layout

Với @media (min-width: 480px) bạn nhận một câu trả lời cho cả ba instance — vì chúng chia sẻ cùng viewport. Card sidebar nhận layout ngang không vừa; card main vẫn xếp dọc dù còn chỗ mở rộng.

Before container queries, teams patched this with:

ApproachVấn đề
Duplicate components (ProductCardCompact, ProductCardWide)Bùng nổ prop, lệch giữa variant
Parent passes a layout="compact" propMọi parent phải biết breakpoint; phá encapsulation
ResizeObserver + class toggling in JSFlash layout, hydration mismatch SSR, gánh nặng test
@media tuned to “most common” viewportSai trong sidebar, modal, split pane, UI zoom

Container query đưa breakpoint vào CSS của component — nơi kiến thức layout thuộc về. Card hỏi ancestor container “tôi có bao nhiêu không gian inline?” rồi thích ứng.

Insight principal: Responsive design không phải “mobile vs desktop.” Mà là mật độ theo ngữ cảnh — bao nhiêu thông tin vừa hộp component được cấp. Container query mã hóa điều đó trực tiếp.


2. The container query model

Query container là element ancestor bạn đánh dấu bằng container-type (và tuỳ chọn container-name). Descendant trong subtree đó dùng rule @container khớp kích thước element đó (hoặc custom property, với style query).

┌─────────────────────────────────────────────┐
  .card-slot  ← query container
  │  container-type: inline-size
  │  container-name: card

  │  ┌─────────────────────────────────────┐
  │  │  .product-card  ← styled by @container │
  │  │  (cannot be the container itself)      │
  │  └─────────────────────────────────────┘
└─────────────────────────────────────────────┘

Viewport width: 1280px  ← ignored by @container
Container width: 320px  ← what @container (min-width: 280px) evaluates

Luồng đánh giá lúc style:

  1. Browser tìm ancestor container đủ điều kiện gần nhất cho mỗi rule @container.
  2. Nếu rule chỉ định container-name, phải khớp ancestor có tên.
  3. Điều kiện kích thước so với query size của container — thường là inline size khi dùng containment inline-size.
  4. Rule khớp cascade như CSS thường; rule sau override rule trước cùng specificity.

3. container-type: inline-size, size, normal

container-type khai báo descendant có thể query kích thước nào.

ValueQueries availableContainment appliedTypical use
normalNone (default)NoneNon-container elements
inline-sizeWidth / inline axisSize containment on inline axisMost UI components — cards, alerts, nav items
sizeWidth and heightFull size containmentCharts, maps, fixed-aspect panels where height queries matter
inline-size size (shorthand)Both axes explicitlyCombinedRare; prefer size when you need both
.card-slot {
  container-type: inline-size;
  /* shorthand equivalent: */
  /* container: card / inline-size; */
}

The layout-containment cost

Đặt container-type: inline-size hoặc size áp dụng layout containment trên trục tương ứng. Đó không phải semantics miễn phí — nó đổi hành vi layout:

  • Container trở thành containing block cho descendant absolute trong nhiều trường hợp hơn.
  • Chiều cao phần trăm trên child có thể resolve khác vì block size container không còn gắn content cùng cách dưới size containment.
  • Float và margin-collapsing có thể đổi tại ranh giới containment.

Quy tắc ngón tay cái: Bắt đầu chỉ inline-size. Dùng size khi thực sự cần @container (min-height: …) — vd widget đổi layout khi panel cao thêm, không chỉ rộng hơn.

Với danh sách card, inline-size hầu như luôn đúng: query width, tránh block-axis containment nặng hơn trừ khi cần.


4. container-name and named @container rules

Container ẩn danh ổn khi mỗi component có một wrapper rõ ràng. Container có tên quan trọng khi có lồng query context hoặc nhiều container trong một subtree.

.dashboard-panel {
  container-type: inline-size;
  container-name: panel;
}

.product-card-wrapper {
  container-type: inline-size;
  container-name: card;
}

/* Respond to the CARD slot, not the outer panel */
@container card (min-width: 400px) {
  .product-card {
    grid-template-columns: 140px 1fr auto;
  }
}

/* Respond to the PANEL — e.g. show a toolbar when the whole panel is wide */
@container panel (min-width: 720px) {
  .panel-toolbar {
    display: flex;
  }
}

Shorthand:

.product-card-wrapper {
  container: card / inline-size;
  /* name: card, type: inline-size */
}
SyntaxMeaning
@container (min-width: 400px)Nearest ancestor with any non-normal container-type
@container card (min-width: 400px)Nearest ancestor named card
@container panel (400px <= inline-size <= 800px)Range syntax (modern browsers)

Nếu không tìm thấy container có tên, block @container bị bỏ qua — nguồn bug “query không bao giờ chạy” phổ biến.


5. Container query units (cqw, cqi, cqh, …)

Container query units tương tự vw/vh với viewport — nhưng tương đối query container.

UnitDefinition
cqw1% of query container width
cqh1% of query container height
cqi1% of query container inline size
cqb1% of query container block size
cqminmin(cqi, cqb)
cqmaxmax(cqi, cqb)
.product-title {
  /* Scale with the card slot, clamped for accessibility */
  font-size: clamp(0.875rem, 4.5cqi, 1.25rem);
}

.product-badge {
  width: 12cqw;
  max-width: 3rem;
}

Use case team senior thực sự ship:

  • Typography fluid trong component không cần bậc @media
  • Icon/avatar tỉ lệ khung widget
  • Padding nội bộ tăng theo container cho mode dày vs thoáng

Gotcha: Query units resolve theo query container đã chọn. Quên container-type trên wrapper thì cqi fallback hành vi initial containing block (gần viewport), trông “gần đúng” khi dev và sai trong sidebar.


6. Style queries — @container style(...) in 2025–2026

Size query trả lời “lớn bao nhiêu?” Style query trả lời “container đang theme/variant gì?”.

.card-slot {
  container-type: inline-size;
  --variant: default;
}

.card-slot[data-variant="featured"] {
  --variant: featured;
}

@container style(--variant: featured) {
  .product-card {
    border-color: var(--color-accent);
  }

  .product-title {
    font-weight: 700;
  }
}

Tách visual variant khỏi size — card featured có accent dù sidebar hẹp, miễn container expose --variant.

Browser support snapshot (2025–2026)

FeatureChromiumFirefoxSafari
@container size queries✅ Stable✅ Stable✅ Stable (16+)
Container query units✅ Stable✅ Stable✅ Stable
@container style(...) custom properties✅ Stable✅ Stable (128+)✅ Stable (18+)
@container style(...) non-custom properties (e.g. display)⚠️ Limited / evolving⚠️ Limited⚠️ Limited

Production nên coi style query custom property là tập được hỗ trợ. Query computed property tùy ý vẫn không nhất quán — nên expose ý định qua --layout-mode: grid trên container.

Combine size + style when both matter:

@container card (min-width: 480px) {
  .product-card { flex-direction: row; }
}

@container style(--variant: featured) {
  .product-card { outline: 2px solid var(--color-accent); }
}

7. Gotchas, containment side effects, and debugging

You cannot query the element itself

Element có container-type không được style bởi rule @container nhắm chính nó — chỉ descendant.

<!-- ❌ @container rules on .card won't apply TO .card -->
<article class="card" style="container-type: inline-size">…</article>

<!-- ✅ wrapper is the container; card is the query subject -->
<div class="card-slot">
  <article class="card">…</article>
</div>

Nếu design không có wrapper thừa, dùng inner wrapper hoặc chấp nhận một node DOM làm ranh giới query — chi phí một div, lợi ích encapsulation đúng.

Ancestor must establish a size

Child width phần trăm trong flex/grid track không xác định có thể để container 0px hoặc min-content — mọi rule @container (min-width: …) fail im lặng. Sửa layout parent trước: cho slot min-width: 0, flex: 1 rõ, hoặc grid track xác định.

@container does not replace all @media

Dùng media query cho concern toàn cục: gutter trang, mode navigation, print, prefers-reduced-motion, prefers-color-scheme. Dùng container query cho mật độ component trong slot biến đổi.

ConcernTool
Sidebar vs top nav at 768px viewport@media
Card horizontal vs stacked in 280px vs 480px slot@container
Dark mode@media (prefers-color-scheme) or :root tokens
Featured variant styling@container style(--variant: featured)

Debugging in DevTools

Chrome và Firefox DevTools (2025+) có container overlay và liệt rule @container khớp cho element chọn. Nếu rule “not matched,” đi lên cây: có container-type? Tên đúng? Inline size computed của container đúng kỳ vọng?

Tác dụng phụ containment: container-type: size ngăn block size container phụ thuộc chiều cao descendant theo cách thường. Container size không height explicit có thể sụp hoặc clip bất ngờ. Ưu tiên inline-size đến khi có chiến lược height đo được.


8. Real component: product card in a dashboard grid

Dưới đây là pattern có thể đưa vào design system: một ProductCard, ba tầng @container, không listener resize JS.

<div class="product-slot">
  <article class="product-card">
    <img class="product-thumb" src="/img/kb-pro.webp" alt="" />
    <div class="product-body">
      <h3 class="product-title">Mechanical Keyboard — Tactile Pro</h3>
      <p class="product-sku">SKU · KB-7742</p>
      <p class="product-desc">Hot-swappable switches, gasket mount.</p>
      <footer class="product-actions">
        <span class="product-price">$189</span>
        <button type="button">Add to cart</button>
      </footer>
    </div>
  </article>
</div>
.product-slot {
  container: product / inline-size;
  min-width: 0; /* critical in flex/grid parents */
}

.product-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.product-thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.product-desc,
.product-actions {
  display: none;
}

@container product (min-width: 280px) {
  .product-card {
    flex-direction: row;
    align-items: flex-start;
  }

  .product-thumb {
    width: 5.5rem;
    aspect-ratio: 1;
    flex-shrink: 0;
  }
}

@container product (min-width: 480px) {
  .product-desc,
  .product-actions {
    display: flex;
  }

  .product-title {
    font-size: clamp(0.875rem, 4cqi, 1.125rem);
  }
}

Cùng markup render compact trong cột kanban, ngang trong rail catalog, mở rộng trong grid main — không cần parent truyền variant="wide".

Migration from ResizeObserver

If you have legacy code:

const ro = new ResizeObserver(([entry]) => {
  el.classList.toggle("is-wide", entry.contentRect.width >= 480);
});
ro.observe(el);

Thay bằng wrapper + @container và xóa observer. Lợi ích: không FOUC first paint, không mismatch class SSR/client, style ở CSS nơi designer và linter thấy được.


9. Live demo

Demo dưới bọc product card trong query container có thể đổi kích thước (240px–700px). Kéo handle hoặc slider — card chuyển compact → horizontal → expanded theo chiều rộng container, readout live hiện viewport width để đối chiếu. Bật cqi units để thấy title scale theo container thay vì bậc rem cố định.

Mở demo đầy đủ:


10. Decision checklist

Trước khi thêm container-type lên wrapper, chạy qua checklist:

  • Component xuất hiện nhiều ngữ cảnh width trên một trang?
  • Query container là ancestor, không phải element đang style?
  • Slot có inline size xác định (flex min-width: 0, grid track, width explicit)?
  • Bắt đầu inline-size trước khi dùng size?
  • Container có tên khi lồng query context?
  • Breakpoint toàn cục vẫn xử lý bằng @media / prefers-* khi phù hợp?
  • Đã verify trong DevTools rule @container nào khớp ở slot hẹp, giữa, rộng?

Container query không giết media query — chúng hoàn thiện responsive design bằng cách component trung thực về không gian được kế thừa. Với team senior, lợi ích là ít prop, ít resize JS, layout sống sót sidebar, split view, dashboard dày mà không fork component.