Advanced CSS Grid — Subgrid, Masonry, auto-fit vs auto-fill, and Layout Patterns
Senior CSS Grid deep-dive: auto-fit vs auto-fill, subgrid, masonry proposals, dense packing, named areas, alignment — with an interactive demo.
Why Grid Still Deserves a Deep Dive {Tại sao Grid vẫn đáng học sâu}
Flexbox solved one-dimensional distribution {Flexbox giải quyết phân phối một chiều}. CSS Grid is the two-dimensional layout engine {CSS Grid là engine layout hai chiều} — rows and columns at once {hàng và cột cùng lúc}, with explicit track sizing, named areas, and item placement that Flexbox cannot express cleanly {với kích thước track rõ ràng, vùng đặt tên, và đặt item mà Flexbox không diễn đạt gọn được}.
Most teams use Grid for card galleries and page shells {Hầu hết team dùng Grid cho gallery card và page shell}, but stop at repeat(auto-fill, minmax(…)) {nhưng dừng ở repeat(auto-fill, minmax(…))}. The features in Grid Level 2 and the emerging Grid Level 3 proposals {Các tính năng trong Grid Level 2 và đề xuất Grid Level 3 đang hình thành} — subgrid, masonry, dense packing, named lines — solve real production pain {subgrid, masonry, dense packing, named lines — giải quyết pain production thật} that JavaScript hacks used to paper over {mà hack JavaScript từng che đi}.
This post is a Grid-only deep dive {Bài này là deep dive chỉ về Grid}. It does not rehash general CSS optimization or performance tuning {Không lặp lại tối ưu CSS chung hay performance tuning}.
Live Demo {Demo trực tiếp}
The interactive demo below covers four advanced patterns {Demo tương tác dưới đây gồm bốn pattern nâng cao}: auto-fit vs auto-fill with a width slider {auto-fit vs auto-fill kèm slider chiều rộng}, grid-template-areas with a desktop/mobile toggle {grid-template-areas với toggle desktop/mobile}, subgrid alignment on product cards {subgrid căn hàng trên product card}, and explicit item placement with span controls {và đặt item rõ ràng với điều khiển span}. Each panel shows a live CSS readout {Mỗi panel hiện CSS readout trực tiếp}.
Open the full demo {Mở demo đầy đủ}: /tools/css-grid-advanced-demo/.
Explicit vs Implicit Grid {Grid rõ ràng vs ngầm định}
When you set display: grid, you define an explicit grid {bạn định nghĩa grid rõ ràng} — the tracks you declare in grid-template-columns, grid-template-rows, and grid-template-areas {các track khai báo trong grid-template-columns, grid-template-rows, và grid-template-areas}.
Anything that overflows those tracks falls into the implicit grid {Phần tràn ra ngoài track đó rơi vào grid ngầm định}, sized by grid-auto-columns and grid-auto-rows {kích thước bởi grid-auto-columns và grid-auto-rows} and placed by grid-auto-flow {và đặt bởi grid-auto-flow}.
.grid {
display: grid;
/* explicit: 3 columns, 2 rows */
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr;
/* implicit: anything beyond 2 rows gets 80px height */
grid-auto-rows: 80px;
grid-auto-flow: row; /* default — fill row by row */
}
| Concept | Controlled by | Default behavior |
|---|---|---|
| Explicit columns | grid-template-columns | Required to activate grid |
| Explicit rows | grid-template-rows | none — rows grow implicitly |
| Implicit column size | grid-auto-columns | auto |
| Implicit row size | grid-auto-rows | auto |
| Overflow placement | grid-auto-flow | row |
Mental model {Mô hình tư duy}:
grid-template-*is your blueprint {grid-template-*là bản thiết kế};grid-auto-*is the fallback factory line for overflow items {grid-auto-*là dây chuyền dự phòng cho item tràn}.
repeat() and minmax() — Responsive Tracks Without Media Queries {repeat() và minmax() — Track responsive không cần media query}
repeat(n, track-list) expands a pattern {mở rộng một pattern}. Combined with minmax(min, max), it creates fluid tracks {tạo track co giãn} that respect a minimum size and grow to fill space {tôn trọng kích thước tối thiểu và giãn để lấp chỗ}.
.card-grid {
display: grid;
grid-template-columns: repeat(4, 1fr); /* fixed 4 equal columns */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* fluid */
}
Common minmax() patterns {Pattern minmax() phổ biến}:
| Pattern | Effect |
|---|---|
minmax(200px, 1fr) | At least 200px, grows to fill |
minmax(0, 1fr) | Can shrink below content size (fixes overflow) |
minmax(min(100%, 300px), 1fr) | Cap max contribution with min() |
repeat(3, minmax(0, 1fr)) | Three equal columns that won’t overflow |
The minmax(0, 1fr) trick matters {Mẹo minmax(0, 1fr) quan trọng}: a bare 1fr track has an implicit minimum of auto (content size) {track 1fr thuần có minimum ngầm là auto (kích thước content)}, which prevents shrinking and causes horizontal overflow in nested layouts {ngăn co lại và gây tràn ngang trong layout lồng nhau}. Setting the minimum to 0 lets the track actually shrink {Đặt minimum là 0 cho phép track thực sự co}.
auto-fit vs auto-fill — The Difference That Matters {auto-fit vs auto-fill — Khác biệt quan trọng}
Both keywords work inside repeat() when the track list would create more columns than fit in the container {Cả hai keyword hoạt động trong repeat() khi danh sách track tạo nhiều cột hơn vừa container}. The behavior diverges when empty tracks remain after all items are placed {Hành vi khác nhau khi còn track trống sau khi đặt hết item}.
/* Both create as many 250px-minimum columns as fit */
.auto-fill { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
.auto-fit { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
| Keyword | Empty tracks | Item behavior | Use when |
|---|---|---|---|
auto-fill | Kept — still occupy space | Items stay at minmax minimum width | You expect more items to load (pagination, infinite scroll) |
auto-fit | Collapsed to 0 width | Items stretch to fill the row | Fixed item count — you want them to grow |
Concrete example {Ví dụ cụ thể}: a 900px container with three 250px-minimum cards {container 900px với ba card minimum 250px}:
auto-fill→ 3 tracks created, 3 items at ~250px each, one empty 250px track remains on the right {3 track tạo ra, 3 item ~250px mỗi cái, một track trống 250px còn lại bên phải}auto-fit→ 3 tracks created, empty track collapses, 3 items stretch to ~300px each {3 track tạo ra, track trống co lại, 3 item giãn ~300px mỗi cái}
Rule of thumb {Quy tắc ngón tay cái}: gallery with unknown item count →
auto-fill{gallery số item không xác định →auto-fill}. Hero feature grid with exactly 3 cards →auto-fit{grid feature cố định 3 card →auto-fit}.
Drag the width slider in the demo to watch tracks appear and disappear {Kéo slider chiều rộng trong demo để xem track xuất hiện và biến mất} — the visual difference is immediate {khác biệt trực quan là tức thì}.
Named Lines and grid-template-areas {Đường đặt tên và grid-template-areas}
Grid lets you name lines and assign named areas {Grid cho phép đặt tên đường và gán vùng tên}, making layout intent readable in CSS instead of magic numbers {làm ý định layout đọc được trong CSS thay vì số ma thuật}.
Named lines {Đường đặt tên}
.page {
display: grid;
grid-template-columns:
[sidebar-start] 240px
[sidebar-end main-start] 1fr
[main-end];
}
.sidebar { grid-column: sidebar-start / sidebar-end; }
.main { grid-column: main-start / main-end; }
Line names survive repeat() expansion {Tên đường sống sót qua mở rộng repeat()}: repeat(3, [col-start] 1fr [col-end]) creates numbered variants like col-start, col-start 2, etc. {tạo biến thể đánh số như col-start, col-start 2, v.v.}.
grid-template-areas {grid-template-areas}
Areas are a higher-level shorthand {Vùng là shorthand cấp cao hơn} — each string row maps cell names, and . means an empty cell {mỗi hàng chuỗi map tên ô, và . nghĩa là ô trống}:
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
gap: 1rem;
min-height: 100dvh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; overflow: auto; }
.footer { grid-area: footer; }
Responsive reflow is a template swap — same HTML, different area map {Reflow responsive là đổi template — cùng HTML, map vùng khác}:
@media (max-width: 768px) {
.page {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
Constraint {Ràng buộc}: every row in
grid-template-areasmust have the same number of cells {mỗi hàng tronggrid-template-areasphải có cùng số ô}, and a named area must be rectangular (no L-shapes) {và vùng đặt tên phải hình chữ nhật (không hình chữ L)}.
Subgrid — Aligning Nested Grids With the Parent {Subgrid — Căn grid lồng nhau với parent}
The problem {Vấn đề}
You have a row of cards, each with title / description / price {Bạn có hàng card, mỗi card có title / description / price}. Each card is its own grid {Mỗi card là grid riêng}. Without subgrid, row heights are independent {Không có subgrid, chiều cao hàng độc lập} — a short description in card A won’t align its price row with card B’s price row {mô tả ngắn ở card A không căn hàng price với card B}.
Before subgrid, teams used JavaScript measurement, equal-height Flexbox hacks, or fixed row heights {Trước subgrid, team dùng đo JavaScript, hack Flexbox equal-height, hoặc chiều cao hàng cố định}.
The solution {Giải pháp}
Subgrid lets a nested grid inherit the parent’s track lines {Subgrid cho grid lồng nhau kế thừa đường track của parent} on the specified axis(es) {trên trục được chỉ định}:
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, auto); /* shared row tracks */
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 4; /* occupy all 4 parent rows */
grid-template-rows: subgrid; /* inherit parent's row tracks */
gap: 0.5rem;
}
Now .title, .body, and .price sit on the same row tracks across every card {Giờ .title, .body, và .price nằm trên cùng row track trên mọi card}, regardless of content length {bất kể độ dài content}.
Subgrid also works on columns {Subgrid cũng hoạt động trên cột}: grid-template-columns: subgrid for horizontally nested components in a shared column system {cho component lồng ngang trong hệ thống cột chung}.
| Property | Value | Effect |
|---|---|---|
grid-template-rows | subgrid | Inherit parent row tracks |
grid-template-columns | subgrid | Inherit parent column tracks |
grid-row / grid-column | span N | Child must span the parent’s subgrid tracks |
Browser support (2025–2026) {Hỗ trợ trình duyệt (2025–2026)}
Subgrid is Baseline widely available as of 2025 {Subgrid Baseline widely available từ 2025}:
| Engine | Since |
|---|---|
| Firefox | 71 (2019) |
| Safari | 16 (2022) |
| Chrome / Edge | 117 (2023) |
You can use subgrid in production with a Flexbox fallback for legacy browsers {Có thể dùng subgrid production với fallback Flexbox cho trình duyệt cũ}:
.card { display: flex; flex-direction: column; }
@supports (grid-template-rows: subgrid) {
.cards { grid-template-rows: repeat(4, auto); }
.card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
}
}
Toggle subgrid off in the demo to see the misalignment return instantly {Tắt subgrid trong demo để thấy lệch hàng quay lại ngay}.
CSS Masonry — The Long Road to Native Packing {CSS Masonry — Con đường dài tới packing native}
Pinterest-style masonry layouts pack items into columns with no vertical gaps {Layout masonry kiểu Pinterest xếp item vào cột không khoảng trống dọc}. For years this required JS libraries or fragile column-count hacks {Nhiều năm cần thư viện JS hoặc hack column-count dễ vỡ} that break reading order and keyboard navigation {phá thứ tự đọc và điều hướng bàn phím}.
The spec evolution {Tiến hóa spec}
The CSSWG has iterated on syntax for years {CSSWG đã lặp cú pháp nhiều năm}. The timeline roughly looks like this {Timeline xấp xỉ như sau}:
- 2020 — Firefox ships
grid-template-rows: masonrybehind a flag {Firefox shipgrid-template-rows: masonrysau flag} - 2022–2023 — Safari TP experiments with masonry on grid {Safari TP thử masonry trên grid}
- 2025 — CSSWG resolves to reuse grid templating;
display: grid-lanesemerges as the dedicated display value {CSSWG quyết tái dùng grid templating;display: grid-lanesnổi lên làm display value riêng} - 2025–2026 —
item-flow/item-packproperties define packing behavior on the lane axis {item-flow/item-packđịnh nghĩa hành vi packing trên trục lane}
Syntax landscape (2025–2026) {Bức tranh cú pháp (2025–2026)}
Two syntaxes coexist during the transition {Hai cú pháp cùng tồn tại trong giai đoạn chuyển tiếp}:
Legacy / transitional — masonry on grid rows {Cũ / chuyển tiếp — masonry trên grid rows}:
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-template-rows: masonry; /* experimental — check support */
gap: 1rem;
}
Emerging — dedicated grid-lanes display {Mới — display grid-lanes riêng}:
.gallery {
display: grid; /* fallback for older browsers */
display: grid-lanes; /* masonry when supported */
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
Packing control via item-flow {Điều khiển packing qua item-flow}:
.gallery {
display: grid-lanes;
grid-template-columns: repeat(3, 1fr);
item-flow: row dense; /* or: item-pack: dense */
gap: 1rem;
}
| Property | Purpose | Status (2026) |
|---|---|---|
grid-template-rows: masonry | Masonry on row axis within grid | Experimental; Safari TP / flags |
display: grid-lanes | Dedicated masonry display mode | Spec draft; early Safari builds |
item-flow / item-pack | Control packing direction and density | Spec draft |
| JS libraries (Masonry.js, etc.) | Production fallback | Stable, but layout-intrinsic |
Do not ship masonry-only layouts without a fallback {Đừng ship layout chỉ masonry mà không fallback}. Use progressive enhancement {Dùng progressive enhancement}:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
align-items: start; /* decent non-masonry fallback */
}
@supports (grid-template-rows: masonry) {
.card-grid {
grid-template-rows: masonry;
}
}
@supports (display: grid-lanes) {
.card-grid {
display: grid-lanes;
}
}
Regular grid with align-items: start gives a reasonable stepped layout in unsupported browsers {Grid thường với align-items: start cho layout bậc thang chấp nhận được ở trình duyệt không hỗ trợ}. Masonry is a visual enhancement, not a layout requirement {Masonry là cải thiện trực quan, không phải yêu cầu layout}.
grid-auto-flow: dense — Filling Holes {grid-auto-flow: dense — Lấp lỗ}
When items have mixed spans, auto-placement can leave gaps in the grid {Khi item có span hỗn hợp, auto-placement có thể để lỗ trong grid}. grid-auto-flow: dense tells the browser to backfill smaller items into earlier gaps {grid-auto-flow: dense bảo trình duyệt lấp item nhỏ hơn vào lỗ trước đó}.
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-auto-flow: dense; /* default is 'row' — no backfill */
gap: 1rem;
}
.wide { grid-column: span 2; }
.tall { grid-row: span 2; }
.hero { grid-column: span 2; grid-row: span 2; }
| Value | Behavior | Trade-off |
|---|---|---|
row (default) | Fill left-to-right, top-to-bottom | Predictable DOM order in visual layout |
column | Fill top-to-bottom, left-to-right | Useful for vertical magazine layouts |
dense | Backfill gaps with later items | Visual density ↑, visual order ≠ DOM order |
Accessibility warning {Cảnh báo accessibility}:
densereorders items visually without changing DOM order {densesắp xếp lại item trực quan mà không đổi thứ tự DOM}. Tab focus sequence follows DOM, not visual position {Thứ tự tab focus theo DOM, không theo vị trí trực quan}. Use only for decorative grids where order doesn’t matter (photo galleries) {Chỉ dùng cho grid trang trí mà thứ tự không quan trọng (gallery ảnh)}, or provide explicitorder/ tabindex management {hoặc quản lýorder/ tabindex rõ ràng}.
Explicit Item Placement {Đặt item rõ ràng}
Auto-placement is the default {Auto-placement là mặc định}, but Grid shines when you place items precisely {nhưng Grid mạnh khi bạn đặt item chính xác}:
.grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(4, 100px);
}
.hero {
grid-column: 1 / span 3; /* start at line 1, span 3 tracks */
grid-row: 1 / span 2;
}
/* equivalent shorthand */
.hero {
grid-column: span 3;
grid-row: span 2;
}
Placement properties {Thuộc tính placement}:
| Property | Shorthand | Meaning |
|---|---|---|
grid-column-start / grid-column-end | grid-column | Column line range |
grid-row-start / grid-row-end | grid-row | Row line range |
grid-area | — | Named area or row-start / col-start / row-end / col-end |
Use line numbers, named lines, or named areas — they compose freely {Dùng số đường, đường đặt tên, hoặc vùng đặt tên — chúng kết hợp tự do}. Negative line numbers count from the end {Số đường âm đếm từ cuối}: grid-column: 1 / -1 spans full width {: 1 / -1 span full width}.
The demo’s span controls show how one hero item reshapes the auto-flow of everything else {Điều khiển span trong demo cho thấy một hero item thay hình auto-flow của mọi thứ khác}.
Alignment — justify-*, align-*, and place-* {Căn chỉnh — justify-*, align-*, và place-*}
Grid alignment operates on two axes {Căn chỉnh Grid trên hai trục}:
- Inline axis (columns in horizontal writing mode) →
justify-*{Trục inline (cột trong chế độ viết ngang) →justify-*} - Block axis (rows) →
align-*{Trục block (hàng) →align-*}
Items inside their cells {Item trong ô của chúng}
.grid {
display: grid;
justify-items: center; /* inline axis — horizontal centering */
align-items: center; /* block axis — vertical centering */
place-items: center; /* shorthand for both */
}
The whole grid inside its container {Toàn grid trong container}
When the grid is smaller than its container, justify-content and align-content distribute extra space between or around tracks {Khi grid nhỏ hơn container, justify-content và align-content phân phối khoảng trống thừa giữa hoặc quanh track}:
.grid {
display: grid;
grid-template-columns: repeat(3, 200px);
min-height: 100dvh;
justify-content: center; /* center tracks horizontally */
align-content: center; /* center tracks vertically */
place-content: center; /* shorthand — the classic centering trick */
}
| Property | Axis | Scope |
|---|---|---|
justify-items / align-items | inline / block | Items within their grid area |
justify-self / align-self | inline / block | Single item override |
justify-content / align-content | inline / block | Tracks within the grid container |
place-items | both | Shorthand for align-items + justify-items |
place-content | both | Shorthand for align-content + justify-content |
place-content: center on a grid with fixed-size tracks is one of the few reliable CSS centering patterns that predates Flexbox ubiquity {place-content: center trên grid với track kích thước cố định là một trong ít pattern căn giữa CSS đáng tin cậy trước khi Flexbox phổ biến}.
Putting It Together — A Production Grid Stack {Kết hợp — Stack Grid production}
A realistic dashboard shell combining the patterns above {Shell dashboard thực tế kết hợp pattern trên}:
.dashboard {
display: grid;
grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
grid-template-rows: auto 1fr;
grid-template-areas:
"nav header"
"nav main";
min-height: 100dvh;
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"nav"
"main";
}
}
.widget-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
gap: 1.5rem;
grid-auto-flow: dense;
}
.widget-grid .hero-widget {
grid-column: span 2;
grid-row: span 2;
}
@supports (grid-template-rows: subgrid) {
.widget-grid {
grid-template-rows: repeat(3, auto);
}
.widget {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
gap: 0.75rem;
}
}
This stack uses {Stack này dùng}:
- Named areas for the shell {Vùng đặt tên cho shell}
auto-fit+minmaxfor responsive widgets {auto-fit+minmaxcho widget responsive}denseto pack mixed-size widgets {denseđể xếp widget kích thước hỗn hợp}subgrid(progressive) for aligned widget internals {subgrid(progressive) cho phần bên trong widget căn hàng}
Decision Cheatsheet {Bảng quyết định nhanh}
| Need | Reach for |
|---|---|
| Fluid card columns, unknown count | repeat(auto-fill, minmax(…)) |
| Fluid columns, fixed item count | repeat(auto-fit, minmax(…)) |
| Page layout with header/sidebar/main | grid-template-areas |
| Align rows across sibling cards | grid-template-rows: subgrid |
| Pinterest-style packing (2026) | grid-lanes / masonry with @supports fallback |
| Fill gaps with mixed-size tiles | grid-auto-flow: dense (a11y caution) |
| Center a fixed-size grid in viewport | place-content: center |
| One item breaks the rhythm | grid-column / grid-row span |
Key Takeaways {Điểm chính}
auto-fitcollapses empty tracks;auto-fillkeeps them {auto-fitco track trống;auto-fillgiữ chúng} — pick based on whether items should stretch or you expect more to arrive {chọn theo item nên giãn hay bạn chờ thêm item}.grid-template-areasis the most readable way to define page shells {grid-template-areaslà cách đọc được nhất để định nghĩa page shell} — swap the template for responsive, not the HTML {đổi template cho responsive, không đổi HTML}.- Subgrid solves cross-card row alignment without JavaScript {Subgrid giải quyết căn hàng cross-card không cần JavaScript} — production-ready in all major browsers since 2023 {sẵn sàng production trên mọi trình duyệt lớn từ 2023}.
- Masonry is coming but still experimental in 2026 {Masonry đang tới nhưng vẫn thử nghiệm năm 2026} — write progressive
@supportsfallbacks, never masonry-only {viết fallback@supportsprogressive, không bao giờ chỉ masonry}. grid-auto-flow: denseimproves visual density at the cost of visual/DOM order divergence {grid-auto-flow: densecải thiện mật độ trực quan đánh đổi lệch thứ tự trực quan/DOM} — use deliberately {dùng có chủ đích}.
Grid is no longer “the new thing” — it is the default tool for two-dimensional layout {Grid không còn là “thứ mới” — nó là công cụ mặc định cho layout hai chiều}. The advanced features in this post are what separate a grid that works from a grid that scales with your design system {Các tính năng nâng cao trong bài là thứ tách grid chạy được khỏi grid scale cùng design system}.