jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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 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-columnsgrid-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 */
}
ConceptControlled byDefault behavior
Explicit columnsgrid-template-columnsRequired to activate grid
Explicit rowsgrid-template-rowsnone — rows grow implicitly
Implicit column sizegrid-auto-columnsauto
Implicit row sizegrid-auto-rowsauto
Overflow placementgrid-auto-flowrow

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()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}:

PatternEffect
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)); }
KeywordEmpty tracksItem behaviorUse when
auto-fillKept — still occupy spaceItems stay at minmax minimum widthYou expect more items to load (pagination, infinite scroll)
auto-fitCollapsed to 0 widthItems stretch to fill the rowFixed 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 đườnggá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-areas must have the same number of cells {mỗi hàng trong grid-template-areas phả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}.

PropertyValueEffect
grid-template-rowssubgridInherit parent row tracks
grid-template-columnssubgridInherit parent column tracks
grid-row / grid-columnspan NChild 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}:

EngineSince
Firefox71 (2019)
Safari16 (2022)
Chrome / Edge117 (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}:

  1. 2020 — Firefox ships grid-template-rows: masonry behind a flag {Firefox ship grid-template-rows: masonry sau flag}
  2. 2022–2023 — Safari TP experiments with masonry on grid {Safari TP thử masonry trên grid}
  3. 2025 — CSSWG resolves to reuse grid templating; display: grid-lanes emerges as the dedicated display value {CSSWG quyết tái dùng grid templating; display: grid-lanes nổi lên làm display value riêng}
  4. 2025–2026item-flow / item-pack properties 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;
}
PropertyPurposeStatus (2026)
grid-template-rows: masonryMasonry on row axis within gridExperimental; Safari TP / flags
display: grid-lanesDedicated masonry display modeSpec draft; early Safari builds
item-flow / item-packControl packing direction and densitySpec draft
JS libraries (Masonry.js, etc.)Production fallbackStable, 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; }
ValueBehaviorTrade-off
row (default)Fill left-to-right, top-to-bottomPredictable DOM order in visual layout
columnFill top-to-bottom, left-to-rightUseful for vertical magazine layouts
denseBackfill gaps with later itemsVisual density ↑, visual order ≠ DOM order

Accessibility warning {Cảnh báo accessibility}: dense reorders items visually without changing DOM order {dense sắ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 explicit order / 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}:

PropertyShorthandMeaning
grid-column-start / grid-column-endgrid-columnColumn line range
grid-row-start / grid-row-endgrid-rowRow line range
grid-areaNamed 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-contentalign-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 */
}
PropertyAxisScope
justify-items / align-itemsinline / blockItems within their grid area
justify-self / align-selfinline / blockSingle item override
justify-content / align-contentinline / blockTracks within the grid container
place-itemsbothShorthand for align-items + justify-items
place-contentbothShorthand 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 + minmax for responsive widgets {auto-fit + minmax cho widget responsive}
  • dense to 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}

NeedReach for
Fluid card columns, unknown countrepeat(auto-fill, minmax(…))
Fluid columns, fixed item countrepeat(auto-fit, minmax(…))
Page layout with header/sidebar/maingrid-template-areas
Align rows across sibling cardsgrid-template-rows: subgrid
Pinterest-style packing (2026)grid-lanes / masonry with @supports fallback
Fill gaps with mixed-size tilesgrid-auto-flow: dense (a11y caution)
Center a fixed-size grid in viewportplace-content: center
One item breaks the rhythmgrid-column / grid-row span

Key Takeaways {Điểm chính}

  1. auto-fit collapses empty tracks; auto-fill keeps them {auto-fit co track trống; auto-fill giữ 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}.
  2. grid-template-areas is the most readable way to define page shells {grid-template-areas là 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}.
  3. 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}.
  4. Masonry is coming but still experimental in 2026 {Masonry đang tới nhưng vẫn thử nghiệm năm 2026} — write progressive @supports fallbacks, never masonry-only {viết fallback @supports progressive, không bao giờ chỉ masonry}.
  5. grid-auto-flow: dense improves visual density at the cost of visual/DOM order divergence {grid-auto-flow: dense cả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}.