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.
Hai hệ thống layout, một cây quyết định
Mọi senior frontend engineer đều biết cả Flexbox và Grid. Í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.
Bài viết này không phải bảng tra thuộc tính. Đó là một mental model để suy luận layout trong formatting context của browser: 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.
Phân biệt cốt lõi: 1D vs 2D
| Flexbox | Grid | |
|---|---|---|
| Chiều | Một chiều mỗi lần | Hàng và cột cùng lúc |
| Câu hỏi chính | ""Item chảy dọc một đường thế nào?" | ""Mỗi item nằm ở đâu trong ma trận?” |
| Tốt nhất cho | Toolbar, nav bar, card footer, căn giữa, phân bổ không gian dọc một trục | Page shell, dashboard, gallery, form grid, mọi thứ có hai trục căn chỉnh |
| Xuống dòng | Tuỳ chọn qua flex-wrap | Có sẵn — hàng/cột là first-class |
Quy tắc ngón tay cái: 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. 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.
Playground trực tiếp
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. Thay đổi kích thước iframe để quan sát behavior auto-fit vs auto-fill.
Mở demo đầy đủ:
Flexbox: Trục và luồng
Flexbox hoạt động trên flex container với flex items. Mọi thứ khác — alignment, phân bổ, wrap — đều xuất phát từ hai trục.
Trục chính và trục phụ
Trục chính được định nghĩa bởi flex-direction:
flex-direction | Trục chính chạy… | Trục phụ chạy… |
|---|---|---|
row (default) | Trái → phải | Trên → dưới |
row-reverse | Right → left | Top → bottom |
column | Top → bottom | Left → right |
column-reverse | Bottom → top | Left → right |
Đó 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:
justify-*luôn nhắm trục chính;align-*luôn nhắm trục phụ. Direction xoay trục — không đổi tên property.
Kích thước flex item: grow, shrink, basis
Mỗi item có ba nút điều khiển cách chia không gian:
| Property | Làm gì |
|---|---|
flex-grow | Hấp thụ bao nhiêu không gian thừa so với anh em (mặc định 0 = không grow) |
flex-shrink | Nhường bao nhiêu khi chật chỗ |
flex-basis | Kích thước khởi điểm trước khi grow/shrink |
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 */
Mẹo production:
flex: 1mở rộng thànhflex: 1 1 0%— basis0%nghĩa là “bắt đầu từ zero và grow đều”. Khác vớiflex: 1 1 auto, tôn trọng kích thước content làm điểm bắt đầu. Trộn chúng trong một container gây cột không đều tinh vi.
flex-wrap và Flex nhiều dòng
Với flex-wrap: wrap, flex container trở thành multi-line. 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 | Áp dụng cho… |
|---|---|
align-items | Item trong một dòng |
align-content | Các dòng khi wrap (chỉ khi còn không gian trục phụ) |
justify-content | Item dọc trục chính (mỗi dòng khi wrap) |
Grid: Track, cell, và implicit grid
CSS Grid định nghĩa hệ thống track hai chiều. Bạn khai báo hàng và cột; item được đặt vào cell.
Grid explicit vs implicit
Explicit grid là những gì bạn định nghĩa bằng grid-template-rows / grid-template-columns. 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 | Nghĩa |
|---|---|
| Explicit tracks | Khai báo trong grid-template-* |
| Implicit tracks | Tạo cho item tràn |
grid-auto-flow: row (default) | Lấp hàng trước, rồi hàng mới |
grid-auto-flow: column | Lấp cột trước |
grid-auto-flow: dense | Lấp khoảng trống bằng item nhỏ hơn |
Đơn vị fr và minmax()
Đơ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) đặt khoảng kích thước track:
| Track definition | Behavior |
|---|---|
1fr | Chia đều không gian còn lại |
minmax(200px, 1fr) | Tối thiểu 200px, grow để lấp |
minmax(0, 1fr) | Có thể shrink dưới min-size content (hữu ích cho overflow) |
min-content | Hẹp bằng chuỗi không break dài nhất |
max-content | Rộng bằng content muốn |
auto-fit vs auto-fill
Cả hai lặp pattern track bao nhiêu lần vừa khít. Khác biệt là điều gì xảy ra với track rỗng:
| Keyword | Empty tracks | Hiệu ứng |
|---|---|---|
auto-fill | Giữ trong grid | Item giữ width minmax; cột rỗng còn là ghost space |
auto-fit | Thu về width 0 | 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));
Khi nào dùng cái nào:
auto-fitcho card grid responsive mà item nên grow lấp hàng.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.
Đặt vị trí Grid
Item tự đặt mặc định. Để đặ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 xuất sắc cho page shell mà topology ổn định. Với grid cấp component và số item động, ưu tiên repeat() + minmax().
Intrinsic sizing: Tầng ẩn
Cả Flexbox và Grid đều tương tác với intrinsic sizing keyword. Hiểu chúng ngăn bug “overflow bí ẩn”.
| Keyword | Resolve thành… |
|---|---|
min-content | Kích thước tối thiểu không overflow (từ dài nhất / đoạn không break) |
max-content | 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 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;
}
Trong Grid, minmax(min-content, 1fr) là lỗi phổ biến — minimum min-content chặn track shrink và phá layout responsive. Ưu tiên minmax(0, 1fr) hoặc minimum pixel cụ thể khi cần shrink.
Alignment: Từ vựng chung, ngữ cảnh khác
CSS Box Alignment Level 3 thống nhất alignment property giữa Flexbox và Grid. Tên trùng nhưng trục khác.
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) |
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;
}
Cái bẫy:
justify-content: centertrên flex container căn item dọc trục chính. Trên grid container, nó căn bộ track khi tổng kích thước track < container. Cùng tên property, chủ thể khác.
gap: Property cả hai đều có
gap (và row-gap / column-gap) hoạt động giống nhau trong Flexbox và Grid. 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 */
}
Trước khi gap hỗ trợ Flexbox, ta dùng margin âm trên container và margin dương trên children. Pattern đó đã lỗi thời — dùng gap.
Kết hợp Flexbox và Grid
Chúng không loại trừ nhau. 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;
}
Luồng quyết định:
- Topology trang / section? → Grid với
grid-template-areashoặc track explicit - Một hàng hoặc cột item? → Flexbox
- Cột bằng nhau wrap responsive? → Grid với
repeat(auto-fit, minmax(...)) - Đẩy một item ra rìa (vd “Sign out” trong nav)? → Flexbox với
margin-inline-start: autotrên item đó - Căn giữa thứ có kích thước không biết trong box? → Flexbox hoặc Grid
place-items: center
Các cái bẫy vẫn cắn
1. flex-basis vs width
Khi flex-direction là row, flex-basis map sang width; khi column, map sang height. Đặ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. Bẫy overflow min-width: auto
Flex item mặc định min-width: auto, nghĩa là không shrink dưới min-size content. 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 */
}
Đây là bug Flexbox production #1 sau khi bỏ float. Cell Grid cũng mặc định tương tự — áp min-width: 0 trên grid item cần truncate.
3. Chiều cao phần trăm cần parent xác định
height: 100% chỉ hoạt động khi mọi ancestor trên chuỗi có height explicit. 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 chặn stretch — và collapse
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. Đặt center khiến item shrink-wrap chiều cao content.
5. Grid subgrid cho alignment lồng
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 */
}
Tra nhanh: Bản đồ property
| Mục tiêu | Flexbox | Grid |
|---|---|---|
| Cột bằng nhau, wrap responsive | Awkward — use Grid | repeat(auto-fit, minmax(X, 1fr)) |
| Khoảng cách giữa item một dòng | justify-content: space-between | N/A (use gap or track sizing) |
| Đẩy item cuối ra rìa | margin-inline-start: auto | justify-self: end |
| Page shell full-height | Possible but fragile | grid-template-rows: auto 1fr auto + min-height: 100dvh |
| Căn giữa kích thước không biết | justify-content + align-items: center | place-items: center |
| Sidebar cố định + main fluid | flex: 0 0 240px + flex: 1 | grid-template-columns: 240px 1fr |
Lời kết
Flexbox trả lời “mọi thứ chảy thế nào?”. Grid trả lời “mỗi thứ thuộc về đâu?”. 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.
Playground trên là cách nhanh nhất để thấm mapping từ control sang CSS rule. Chuyển mode, cố tình phá layout, và chú ý trục nào mỗi property thực sự di chuyển — đó là cách mental model ăn sâu.