jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Flexbox vs Grid — The Layout Mental Model Senior Frontend Engineers Actually Need

Flexbox vs Grid mental model for senior engineers: axes, flex shorthand, fr tracks, auto-fit vs auto-fill, intrinsic sizing, and production layout pitfalls.

Two Layout Systems, One Decision Tree {Hai hệ thống layout, một cây quyết định}

Every senior frontend engineer knows both Flexbox and Grid {Mọi senior frontend engineer đều biết cả Flexbox và Grid}. Few can articulate when to reach for which — and fewer still can predict how a container will behave when content, viewport, and min-width: auto collide {Ít người diễn đạt được khi nào nên dùng cái nào — và còn ít hơn nữa là dự đoán container sẽ behave thế nào khi content, viewport, và min-width: auto va chạm}.

This post is not a property cheat sheet {Bài viết này không phải bảng tra thuộc tính}. It is a mental model for reasoning about layout in the browser’s formatting context {Đó là một mental model để suy luận layout trong formatting context của browser}: one dimension vs two, explicit vs implicit tracks, intrinsic vs extrinsic sizing, and the traps that survive code review {một chiều vs hai chiều, track explicit vs implicit, intrinsic vs extrinsic sizing, và các cái bẫy vẫn lọt qua code review}.


The Core Distinction: 1D vs 2D {Phân biệt cốt lõi: 1D vs 2D}

FlexboxGrid
Dimensions {Chiều}One at a time {Một chiều mỗi lần}Row and column simultaneously {Hàng cột cùng lúc}
Primary question {Câu hỏi chính}“How do items flow along a line?” {“Item chảy dọc một đường thế nào?”}”Where does each item sit in a matrix?” {“Mỗi item nằm ở đâu trong ma trận?”}
Best for {Tốt nhất cho}Toolbars, nav bars, card footers, centering, distributing space along one axis {Toolbar, nav bar, card footer, căn giữa, phân bổ không gian dọc một trục}Page shells, dashboards, galleries, form grids, anything with two alignment axes {Page shell, dashboard, gallery, form grid, mọi thứ có hai trục căn chỉnh}
Wrapping {Xuống dòng}Optional via flex-wrap {Tuỳ chọn qua flex-wrap}Built-in — rows/columns are first-class {Có sẵn — hàng/cột là first-class}

Rule of thumb {Quy tắc ngón tay cái}: If you find yourself fighting Flexbox with nested flex containers to simulate a grid, switch to Grid {Nếu bạn đang vật lộn với Flexbox lồng nhau để giả lập grid, hãy chuyển sang Grid}. If you only need to align or distribute items in one direction, Flexbox is simpler and often more resilient to content changes {Nếu bạn chỉ cần căn hoặc phân bổ item theo một hướng, Flexbox đơn giản hơn và thường chịu thay đổi content tốt hơn}.


Live Playground {Playground trực tiếp}

The demo below lets you toggle between Flexbox and Grid, adjust alignment and track properties, and see the generated CSS rule in real time {Demo dưới cho phép bạn chuyển giữa Flexbox và Grid, chỉnh alignment và track property, và xem CSS rule được sinh ra theo thời gian thực}. Resize the iframe to observe auto-fit vs auto-fill behavior {Thay đổi kích thước iframe để quan sát behavior auto-fit vs auto-fill}.

Open the full demo {Mở demo đầy đủ}: /tools/css-layout-playground/.


Flexbox: Axes and Flow {Flexbox: Trục và luồng}

Flexbox operates on a flex container with flex items {Flexbox hoạt động trên flex container với flex items}. Everything else — alignment, distribution, wrapping — derives from two axes {Mọi thứ khác — alignment, phân bổ, wrap — đều xuất phát từ hai trục}.

Main Axis and Cross Axis {Trục chính và trục phụ}

The main axis is defined by flex-direction {Trục chính được định nghĩa bởi flex-direction}:

flex-directionMain axis runs… {Trục chính chạy…}Cross axis runs… {Trục phụ chạy…}
row (default)Left → right {Trái → phải}Top → bottom {Trên → dưới}
row-reverseRight → leftTop → bottom
columnTop → bottomLeft → right
column-reverseBottom → topLeft → right

This is why justify-content and align-items swap semantic meaning when you change direction {Đó là lý do justify-contentalign-items đổi nghĩa khi bạn đổi direction}:

.toolbar {
  display: flex;
  flex-direction: row;
  justify-content: space-between; /* distributes along horizontal main axis */
  align-items: center;            /* centers vertically on cross axis */
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  justify-content: flex-start; /* now vertical — pushes items to top */
  align-items: stretch;        /* now horizontal — items fill width */
}

Mental model {Mental model}: justify-* always targets the main axis; align-* always targets the cross axis {justify-* luôn nhắm trục chính; align-* luôn nhắm trục phụ}. Direction rotates the axes — it does not rename the properties {Direction xoay trục — không đổi tên property}.

Flex Item Sizing: grow, shrink, basis {Kích thước flex item: grow, shrink, basis}

Each item has three knobs that control how it shares space {Mỗi item có ba nút điều khiển cách chia không gian}:

PropertyWhat it does {Làm gì}
flex-growHow much extra space this item absorbs relative to siblings {Hấp thụ bao nhiêu không gian thừa so với anh em} (default 0 = don’t grow) {(mặc định 0 = không grow)}
flex-shrinkHow much this item yields when space is tight {Nhường bao nhiêu khi chật chỗ} (default 1 = can shrink)
flex-basisThe starting size before grow/shrink kicks in {Kích thước khởi điểm trước khi grow/shrink} (default auto = use width/height or content size)

The flex shorthand encodes all three in one declaration {Shorthand flex gom cả ba trong một khai báo}:

/* flex: <grow> <shrink> <basis> */

.sidebar  { flex: 0 0 240px; }  /* fixed 240px, never grow or shrink */
.content  { flex: 1 1 auto; }   /* take remaining space, can shrink */
.tag      { flex: 0 1 auto; }   /* size to content, shrink if needed */
.icon     { flex: none; }       /* shorthand for 0 0 auto — rigid */

Production tip {Mẹo production}: flex: 1 expands to flex: 1 1 0% — the 0% basis means “start from zero and grow equally” {flex: 1 mở rộng thành flex: 1 1 0% — basis 0% nghĩa là “bắt đầu từ zero và grow đều”}. This is different from flex: 1 1 auto, which respects content size as the starting point {Khác với flex: 1 1 auto, tôn trọng kích thước content làm điểm bắt đầu}. Mixing them in one container causes subtle uneven columns {Trộn chúng trong một container gây cột không đều tinh vi}.

flex-wrap and Multi-Line Flex {flex-wrap và Flex nhiều dòng}

With flex-wrap: wrap, a flex container becomes multi-line {Với flex-wrap: wrap, flex container trở thành multi-line}. Each line is its own flex formatting context for cross-axis alignment {Mỗi dòng là một flex formatting context riêng cho cross-axis alignment}.

.chip-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-content: flex-start; /* distributes LINES on cross axis, not items */
}
PropertyApplies to… {Áp dụng cho…}
align-itemsItems within a single line {Item trong một dòng}
align-contentLines when wrapped (only if extra cross-axis space) {Các dòng khi wrap (chỉ khi còn không gian trục phụ)}
justify-contentItems along the main axis (per line when wrapped) {Item dọc trục chính (mỗi dòng khi wrap)}

Grid: Tracks, Cells, and the Implicit Grid {Grid: Track, cell, và implicit grid}

CSS Grid defines a two-dimensional track system {CSS Grid định nghĩa hệ thống track hai chiều}. You declare rows and columns; items are placed into cells {Bạn khai báo hàng và cột; item được đặt vào cell}.

Explicit vs Implicit Grid {Grid explicit vs implicit}

The explicit grid is what you define with grid-template-rows / grid-template-columns {Explicit grid là những gì bạn định nghĩa bằng grid-template-rows / grid-template-columns}. When items overflow those tracks, the browser creates an implicit grid with auto-sized tracks {Khi item tràn track đó, browser tạo implicit grid với track auto-sized}.

.dashboard {
  display: grid;
  grid-template-columns: 240px 1fr;   /* explicit: 2 columns */
  grid-template-rows: auto 1fr auto;  /* explicit: 3 rows */
  /* items beyond row 3 → implicit rows, sized by grid-auto-rows (default: auto) */
  grid-auto-rows: minmax(80px, auto);
}
ConceptMeaning {Nghĩa}
Explicit tracksDeclared in grid-template-* {Khai báo trong grid-template-*}
Implicit tracksCreated for overflow items {Tạo cho item tràn}
grid-auto-flow: row (default)Fill rows first, then new row {Lấp hàng trước, rồi hàng mới}
grid-auto-flow: columnFill columns first {Lấp cột trước}
grid-auto-flow: denseBackfill gaps with smaller items {Lấp khoảng trống bằng item nhỏ hơn}

The fr Unit and minmax() {Đơn vị frminmax()}

The fr (fraction) unit distributes remaining free space after fixed and intrinsic sizes are accounted for {Đơn vị fr (fraction) phân bổ không gian trống còn lại sau khi trừ kích thước cố định và intrinsic}.

.layout {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;
  /* sidebar fixed 200px; two columns split remaining space equally */
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

minmax(min, max) sets a track size range {minmax(min, max) đặt khoảng kích thước track}:

Track definitionBehavior {Behavior}
1frEqual share of leftover space {Chia đều không gian còn lại}
minmax(200px, 1fr)At least 200px, grows to fill {Tối thiểu 200px, grow để lấp}
minmax(0, 1fr)Can shrink below content min-size (useful for overflow) {Có thể shrink dưới min-size content (hữu ích cho overflow)}
min-contentAs narrow as longest unbreakable string {Hẹp bằng chuỗi không break dài nhất}
max-contentAs wide as content wants {Rộng bằng content muốn}

auto-fit vs auto-fill {auto-fit vs auto-fill}

Both repeat a track pattern as many times as will fit {Cả hai lặp pattern track bao nhiêu lần vừa khít}. The difference is what happens to empty tracks {Khác biệt là điều gì xảy ra với track rỗng}:

KeywordEmpty tracksVisual effect {Hiệu ứng}
auto-fillKept in the grid {Giữ trong grid}Items stay their minmax width; empty columns remain as ghost space {Item giữ width minmax; cột rỗng còn là ghost space}
auto-fitCollapsed to 0 width {Thu về width 0}Existing items stretch to fill the row {Item hiện có stretch lấp hàng}
/* 3 cards in a 1200px container with minmax(250px, 1fr): */

/* auto-fill → 4 columns created, 4th is empty, cards stay ~250px+ */
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));

/* auto-fit  → 3 columns, cards expand to fill full width */
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));

When to use which {Khi nào dùng cái nào}: auto-fit for responsive card grids where items should grow to fill the row {auto-fit cho card grid responsive mà item nên grow lấp hàng}. auto-fill when you want a consistent column count and don’t want items to stretch just because a slot is empty {auto-fill khi bạn muốn số cột nhất quán và không muốn item stretch chỉ vì còn slot rỗng}.

Grid Placement {Đặt vị trí Grid}

Items auto-place by default {Item tự đặt mặc định}. For explicit placement, use line numbers or named areas {Để đặt explicit, dùng số line hoặc named area}:

.page {
  display: grid;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  min-height: 100dvh;
}

.page > header  { grid-area: header; }
.page > aside   { grid-area: sidebar; }
.page > main    { grid-area: main; }
.page > footer  { grid-area: footer; }

grid-template-areas is excellent for page shells where the topology is stable {grid-template-areas xuất sắc cho page shell mà topology ổn định}. For component-level grids with dynamic item counts, prefer repeat() + minmax() {Với grid cấp component và số item động, ưu tiên repeat() + minmax()}.


Intrinsic Sizing: The Hidden Layer {Intrinsic sizing: Tầng ẩn}

Both Flexbox and Grid interact with intrinsic sizing keywords {Cả Flexbox và Grid đều tương tác với intrinsic sizing keyword}. Understanding them prevents “mystery overflow” bugs {Hiểu chúng ngăn bug “overflow bí ẩn”}.

KeywordResolves to… {Resolve thành…}
min-contentMinimum size without overflow (longest word / unbreakable run) {Kích thước tối thiểu không overflow (từ dài nhất / đoạn không break)}
max-contentSize if nothing wrapped or constrained {Kích thước nếu không wrap hay bị ràng buộc}
fit-contentmin(max-content, max(min-content, stretch)) — clamp to available space {Clamp theo không gian có sẵn}
.truncate-safe {
  width: fit-content;
  max-width: 100%;
}

.grid-cell {
  /* Allow track to shrink below content min-width */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

In Grid, minmax(min-content, 1fr) is a common mistake — the min-content minimum prevents tracks from shrinking and breaks responsive layouts {Trong Grid, minmax(min-content, 1fr) là lỗi phổ biến — minimum min-content chặn track shrink và phá layout responsive}. Prefer minmax(0, 1fr) or a concrete pixel minimum when you need shrinkability {Ưu tiên minmax(0, 1fr) hoặc minimum pixel cụ thể khi cần shrink}.


Alignment: Shared Vocabulary, Different Contexts {Alignment: Từ vựng chung, ngữ cảnh khác}

CSS Box Alignment Level 3 unified alignment properties across Flexbox and Grid {CSS Box Alignment Level 3 thống nhất alignment property giữa Flexbox và Grid}. The names overlap but the axes differ {Tên trùng nhưng trục khác}.

Flexbox alignment {Alignment Flexbox}

PropertyAxisScope
justify-contentMainAll items in container
align-itemsCrossAll items in container
align-selfCrossSingle item override
align-contentCrossWrapped lines (not items)

Grid alignment {Alignment Grid}

PropertyAxisScope
justify-itemsInline (column)All items in their cells
align-itemsBlock (row)All items in their cells
justify-self / align-selfPer-axisSingle item in its cell
justify-contentInlineEntire grid within container (when grid < container)
align-contentBlockEntire grid within container
/* Center a grid that is smaller than its container */
.modal-grid {
  display: grid;
  grid-template-columns: repeat(3, 120px);
  justify-content: center; /* centers the grid tracks as a group */
  align-content: center;
  min-height: 400px;
}

Gotcha {Cái bẫy}: justify-content: center on a flex container centers items along the main axis {justify-content: center trên flex container căn item dọc trục chính}. On a grid container, it centers the track set when total track size < container size {Trên grid container, nó căn bộ track khi tổng kích thước track < container}. Same property name, different subject {Cùng tên property, chủ thể khác}.


gap: The One Property Both Share {gap: Property cả hai đều có}

gap (and row-gap / column-gap) works identically in Flexbox and Grid {gap (và row-gap / column-gap) hoạt động giống nhau trong Flexbox và Grid}. It creates space between items without affecting edges {Tạo khoảng cách giữa item mà không ảnh hưởng cạnh}.

/* Prefer gap over margin hacks on first/last child */
.card-row {
  display: flex;
  gap: 1rem;
}

.photo-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem 1rem; /* row-gap column-gap */
}

Before gap was supported in Flexbox, we used negative margins on containers and positive margins on children {Trước khi gap hỗ trợ Flexbox, ta dùng margin âm trên container và margin dương trên children}. That pattern is obsolete — use gap {Pattern đó đã lỗi thời — dùng gap}.


Combining Flexbox and Grid {Kết hợp Flexbox và Grid}

They are not mutually exclusive {Chúng không loại trừ nhau}. Production layouts routinely nest them {Layout production thường lồng chúng}:

<!-- Grid for page shell -->
<div class="app">
  <header class="app-header">…</header>
  <aside class="app-sidebar">…</aside>
  <main class="app-main">
    <!-- Flex for toolbar within main -->
    <div class="toolbar">
      <h1>Dashboard</h1>
      <div class="toolbar-actions">…</div>
    </div>
    <!-- Grid for card matrix -->
    <div class="card-grid">…</div>
  </main>
</div>
.app {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr;
  min-height: 100dvh;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

Decision flow {Luồng quyết định}:

  1. Page / section topology? → Grid with grid-template-areas or explicit tracks {Topology trang / section? → Grid với grid-template-areas hoặc track explicit}
  2. Single row or column of items? → Flexbox {Một hàng hoặc cột item? → Flexbox}
  3. Equal-width columns that wrap responsively? → Grid with repeat(auto-fit, minmax(...)) {Cột bằng nhau wrap responsive? → Grid với repeat(auto-fit, minmax(...))}
  4. Push one item to the far edge (e.g. “Sign out” in a nav)? → Flexbox with margin-inline-start: auto on that item {Đẩy một item ra rìa (vd “Sign out” trong nav)? → Flexbox với margin-inline-start: auto trên item đó}
  5. Center something unknown-size in a box? → Flexbox (justify-content: center; align-items: center) or Grid (place-items: center) {Căn giữa thứ có kích thước không biết trong box? → Flexbox hoặc Grid place-items: center}

Common Pitfalls That Still Bite {Các cái bẫy vẫn cắn}

1. flex-basis vs width {flex-basis vs width}

When flex-direction is row, flex-basis maps to width; when column, it maps to height {Khi flex-directionrow, flex-basis map sang width; khi column, map sang height}. Setting width: 200px on a column-direction flex item does not set its main-axis size — flex-basis (or height) does {Đặt width: 200px trên flex item direction column không đặt kích thước trục chính — flex-basis (hoặc height) mới đặt}.

/* ❌ width ignored as main-axis size in column flex */
.sidebar { width: 240px; flex-direction: column; }

/* ✅ */
.sidebar { flex: 0 0 240px; } /* row direction */
.sidebar { flex: 0 0 240px; flex-direction: column; } /* basis → height */

2. The min-width: auto overflow trap {Bẫy overflow min-width: auto}

Flex items default to min-width: auto (min-height in column direction), which means they won’t shrink below their content’s minimum size {Flex item mặc định min-width: auto, nghĩa là không shrink dưới min-size content}. Long unbreakable strings, wide <pre> blocks, or nested flex children cause horizontal overflow {Chuỗi dài không break, block <pre> rộng, hoặc flex con lồng gây overflow ngang}.

.flex-child {
  min-width: 0; /* or overflow: hidden on the flex item */
}

This is the #1 Flexbox production bug after switching from floats {Đây là bug Flexbox production #1 sau khi bỏ float}. Grid cells have the same default — apply min-width: 0 on grid items that must truncate {Cell Grid cũng mặc định tương tự — áp min-width: 0 trên grid item cần truncate}.

3. Percentage heights need a defined parent {Chiều cao phần trăm cần parent xác định}

height: 100% only works when every ancestor up the chain has an explicit height {height: 100% chỉ hoạt động khi mọi ancestor trên chuỗi có height explicit}. Modern fix: use min-height: 100dvh on the page grid and 1fr rows instead of percentage heights {Cách hiện đại: min-height: 100dvh trên page grid và hàng 1fr thay cho height phần trăm}.

/* ❌ fragile */
.page { height: 100%; }
.main  { height: 100%; }

/* ✅ */
.page {
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-height: 100dvh;
}

4. align-items: center prevents stretch — and collapse {align-items: center chặn stretch — và collapse}

When flex items should fill cross-axis space (e.g. equal-height cards in a row), align-items: stretch (default) is correct {Khi flex item nên lấp cross-axis (vd card cùng chiều cao trong hàng), align-items: stretch (mặc định) là đúng}. Setting center makes items shrink-wrap their content height {Đặt center khiến item shrink-wrap chiều cao content}.

5. Grid subgrid for nested alignment {Grid subgrid cho alignment lồng}

When a card grid sits inside a grid row and you need column lines to align across rows of cards, subgrid (now widely supported) lets a nested grid inherit parent track definitions {Khi card grid nằm trong hàng grid và bạn cần line cột căn giữa các hàng card, subgrid (giờ hỗ trợ rộng) cho grid lồng kế thừa track definition của parent}:

.card {
  display: grid;
  grid-row: span 1;
  grid-template-rows: subgrid;
  grid-row: span 3; /* title, body, footer align across cards */
}

Quick Reference: Property Map {Tra nhanh: Bản đồ property}

Goal {Mục tiêu}FlexboxGrid
Equal columns, responsive wrap {Cột bằng nhau, wrap responsive}Awkward — use Gridrepeat(auto-fit, minmax(X, 1fr))
Space between items on one line {Khoảng cách giữa item một dòng}justify-content: space-betweenN/A (use gap or track sizing)
Push last item to edge {Đẩy item cuối ra rìa}margin-inline-start: autojustify-self: end
Full-height page shell {Page shell full-height}Possible but fragilegrid-template-rows: auto 1fr auto + min-height: 100dvh
Unknown-size centering {Căn giữa kích thước không biết}justify-content + align-items: centerplace-items: center
Fixed sidebar + fluid main {Sidebar cố định + main fluid}flex: 0 0 240px + flex: 1grid-template-columns: 240px 1fr

Closing Thought {Lời kết}

Flexbox answers “how do things flow?” {Flexbox trả lời “mọi thứ chảy thế nào?”}. Grid answers “where does each thing belong?” {Grid trả lời “mỗi thứ thuộc về đâu?”}. Master the axes, respect intrinsic sizing, and reach for min-width: 0 before reaching for overflow-x: hidden on the body {Nắm trục, tôn trọng intrinsic sizing, và chọn min-width: 0 trước khi chọn overflow-x: hidden trên body}.

The playground above is the fastest way to internalize the mapping from control to CSS rule {Playground trên là cách nhanh nhất để thấm mapping từ control sang CSS rule}. Toggle modes, break layouts on purpose, and notice which axis each property actually moves {Chuyển mode, cố tình phá layout, và chú ý trục nào mỗi property thực sự di chuyển} — that is how the mental model sticks {đó là cách mental model ăn sâu}.