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}
| Flexbox | Grid | |
|---|---|---|
| Dimensions {Chiều} | One at a time {Một chiều mỗi lần} | Row and column simultaneously {Hàng và 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-direction | Main 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-reverse | Right → left | Top → bottom |
column | Top → bottom | Left → right |
column-reverse | Bottom → top | Left → right |
This is why justify-content and align-items swap semantic meaning when you change direction {Đó là lý do justify-content và align-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}:
| Property | What it does {Làm gì} |
|---|---|
flex-grow | How 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-shrink | How much this item yields when space is tight {Nhường bao nhiêu khi chật chỗ} (default 1 = can shrink) |
flex-basis | The 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: 1expands toflex: 1 1 0%— the0%basis means “start from zero and grow equally” {flex: 1mở rộng thànhflex: 1 1 0%— basis0%nghĩa là “bắt đầu từ zero và grow đều”}. This is different fromflex: 1 1 auto, which respects content size as the starting point {Khác vớiflex: 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 */
}
| Property | Applies to… {Áp dụng cho…} |
|---|---|
align-items | Items within a single line {Item trong một dòng} |
align-content | Lines 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-content | Items 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);
}
| Concept | Meaning {Nghĩa} |
|---|---|
| Explicit tracks | Declared in grid-template-* {Khai báo trong grid-template-*} |
| Implicit tracks | Created 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: column | Fill columns first {Lấp cột trước} |
grid-auto-flow: dense | Backfill gaps with smaller items {Lấp khoảng trống bằng item nhỏ hơn} |
The fr Unit and minmax() {Đơn vị fr và minmax()}
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 definition | Behavior {Behavior} |
|---|---|
1fr | Equal 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-content | As narrow as longest unbreakable string {Hẹp bằng chuỗi không break dài nhất} |
max-content | As 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}:
| Keyword | Empty tracks | Visual effect {Hiệu ứng} |
|---|---|---|
auto-fill | Kept 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-fit | Collapsed 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-fitfor responsive card grids where items should grow to fill the row {auto-fitcho card grid responsive mà item nên grow lấp hàng}.auto-fillwhen you want a consistent column count and don’t want items to stretch just because a slot is empty {auto-fillkhi 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”}.
| Keyword | Resolves to… {Resolve thành…} |
|---|---|
min-content | Minimum 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-content | Size if nothing wrapped or constrained {Kích thước nếu không wrap hay bị ràng buộc} |
fit-content | min(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}
| Property | Axis | Scope |
|---|---|---|
justify-content | Main | All items in container |
align-items | Cross | All items in container |
align-self | Cross | Single item override |
align-content | Cross | Wrapped lines (not items) |
Grid alignment {Alignment Grid}
| Property | Axis | Scope |
|---|---|---|
justify-items | Inline (column) | All items in their cells |
align-items | Block (row) | All items in their cells |
justify-self / align-self | Per-axis | Single item in its cell |
justify-content | Inline | Entire grid within container (when grid < container) |
align-content | Block | Entire 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: centeron a flex container centers items along the main axis {justify-content: centertrê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}:
- Page / section topology? → Grid with
grid-template-areasor explicit tracks {Topology trang / section? → Grid vớigrid-template-areashoặc track explicit} - Single row or column of items? → Flexbox {Một hàng hoặc cột item? → Flexbox}
- Equal-width columns that wrap responsively? → Grid with
repeat(auto-fit, minmax(...)){Cột bằng nhau wrap responsive? → Grid vớirepeat(auto-fit, minmax(...))} - Push one item to the far edge (e.g. “Sign out” in a nav)? → Flexbox with
margin-inline-start: autoon that item {Đẩy một item ra rìa (vd “Sign out” trong nav)? → Flexbox vớimargin-inline-start: autotrên item đó} - 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 Gridplace-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-direction là row, 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} | Flexbox | Grid |
|---|---|---|
| Equal columns, responsive wrap {Cột bằng nhau, wrap responsive} | Awkward — use Grid | repeat(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-between | N/A (use gap or track sizing) |
| Push last item to edge {Đẩy item cuối ra rìa} | margin-inline-start: auto | justify-self: end |
| Full-height page shell {Page shell full-height} | Possible but fragile | grid-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: center | place-items: center |
| Fixed sidebar + fluid main {Sidebar cố định + main fluid} | flex: 0 0 240px + flex: 1 | grid-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}.