Flexbox: Hiểu Từ Bên Trong (Hướng Dẫn Tương Tác)
Hiểu sâu Flexbox qua demo tương tác: từ mental model, justify/align, grow/shrink đến gotcha thực chiến.
Hầu hết dev đều dùng Flexbox hằng ngày, nhưng nhiều người vẫn "đoán mò" mỗi khi layout không như ý. Bài viết này không liệt kê thuộc tính — nó xây dựng mental model đúng đắn về cách Flexbox suy nghĩ từ bên trong. Khi hiểu thuật toán, bạn sẽ không cần nhớ thuộc tính nữa.
Tôi sẽ dùng một hình ảnh xuyên suốt: đoàn tàu trên đường ray. Các toa tàu (flex items) xếp dọc theo ray (trục chính). Mỗi toa có thể cao thấp khác nhau (trục phụ). Mọi quyết định trong Flexbox đều xoay quanh đường ray này.
#Phần 1 — Nhập môn: Flexbox giải quyết vấn đề gì
Trong layout bình thường (flow layout), các phần tử xếp chồng lên nhau (block) hoặc nằm cạnh nhau (inline). Cách xếp này dựa trên thứ tự trong HTML và loại phần tử — bạn gần như không kiểm soát được vị trí tương đối giữa các phần tử với nhau.
Flexbox thay đổi hoàn toàn cách tư duy: bạn khai báo một flex formatting context bằng display: flex, và lập tức các phần tử con trở thành flex items. Chúng biết về nhau. Không gian được phân phối dựa trên mối quan hệ, không phải vị trí tuyệt đối.
.container {
display: flex;
}Khi nào dùng Flexbox, khi nào dùng Grid? Quy tắc đơn giản: Flexbox cho layout một chiều (một trục chính), Grid cho hai chiều (hàng và cột cùng lúc). Navbar, toolbar, card nội dung dọc — Flexbox. Dashboard, gallery ảnh, form phức tạp — Grid. Nếu bạn thấy mình cần canh cả hàng lẫn cột, có thể bạn cần Grid.
Thử toggle display: flex trong demo dưới đây để thấy sự khác biệt ngay lập tức.
Khi bạn bật display: flex, các phần tử từ xếp chồng (block) chuyển sang nằm cạnh nhau trên cùng một hàng. Đó là hành vi mặc định — và cũng là mental model đầu tiên: Flexbox mặc định tạo một đường ray ngang.
#Phần 2 — Trục chính & trục phụ (Primary & Cross Axis)
Đây là khái niệm quan trọng nhất trong Flexbox. Mọi thuộc tính canh chỉnh, phân phối, co giãn đều xoay quanh hai trục:
- Trục chính (primary axis): hướng các flex items xếp cạnh nhau. Mặc định là ngang (
row). - Trục phụ (cross axis): vuông góc với trục chính. Mặc định là dọc.
Quay lại hình ảnh đoàn tàu: đường ray là trục chính, các toa tàu chạy dọc theo ray. Chiều cao của mỗi toa (trục phụ) là thứ yếu — điều quan trọng là các toa xếp theo hướng nào.
flex-direction quyết định hướng của đường ray:
.container {
display: flex;
flex-direction: row; /* ray ngang (mặc định) */
flex-direction: row-reverse; /* ray ngang, đảo ngược */
flex-direction: column; /* ray dọc */
flex-direction: column-reverse; /* ray dọc, đảo ngược */
}Khi bạn đổi flex-direction từ row sang column, đường ray xoay 90 độ. Trục chính thành dọc, trục phụ thành ngang. Tất cả hành vi còn lại (justify, align, grow, shrink) đều đổi trục theo.
Thử chuyển flex-direction trong demo dưới đây. Chú ý cách "trục chính" và "trục phụ" đảo chiều khi bạn thay đổi giá trị.
Takeaway: trước khi viết bất kỳ thuộc tính Flexbox nào, hãy tự hỏi "đường ray đang chạy hướng nào?". Nếu bạn đổi flex-direction: column mà quên rằng justify-content bây giờ canh theo chiều dọc — bạn sẽ mất 30 phút debug vô ích.
#Phần 3 — Canh chỉnh: justify-content & align-items
Hai thuộc tính này thường bị nhầm lẫn. Cách nhớ đơn giản:
justify-content: canh dọc theo đường ray (trục chính).align-items: canh vuông góc với đường ray (trục phụ).
Nghĩa là đoàn tàu đang chạy trên ray, justify-content quyết định các toa cách nhau bao xa trên ray, còn align-items quyết định mỗi toa cao bao nhiêu so với chiều cao toa lớn nhất.
.container {
display: flex;
justify-content: flex-start; /* dồn về đầu ray (mặc định) */
justify-content: center; /* các toa ở giữa ray */
justify-content: space-between; /* toa đầu ở đầu, toa cuối ở cuối, đều nhau */
justify-content: space-around; /* mỗi toa có khoảng cách đều hai bên */
justify-content: space-evenly; /* mọi khoảng cách bằng nhau, kể cả hai đầu */
justify-content: flex-end; /* dồn về cuối ray */
}.container {
display: flex;
align-items: stretch; /* toa cao bằng nhau, bằng container (mặc định) */
align-items: flex-start; /* toa cao tự nhiên, dồn lên đầu */
align-items: center; /* toa cao tự nhiên, căn giữa */
align-items: flex-end; /* toa cao tự nhiên, dồn xuống cuối */
align-items: baseline; /* canh theo đường baseline của text */
}#Tại sao có align-items nhưng không có justify-items?
Đây là câu hỏi hay. Trong Flexbox, trục chính là trục phân phối — các item được đặt cạnh nhau theo một trình tự. Không có khái niệm "mỗi item tự quyết định vị trí trên trục chính" (đó là Grid). Nhưng trên trục phụ, mỗi item có thể tự quyết định vị trí của nó bằng align-self:
.item-special {
align-self: flex-end; /* toa này dồn xuống đáy, các toa khác giữ nguyên */
}Tổng cộng có bốn từ khóa canh chỉnh, tạo thành ma trận 2×2:
| Dọc theo ray (trục chính) | Vuông góc ray (trục phụ) | |
|---|---|---|
| Toàn bộ container | justify-content | align-items |
| Từng item | (không có) | align-self |
Thử hai dropdown trong demo dưới đây để thấy justify-content và align-items tương tác với nhau thế nào.
Takeaway: justify-content điều khiển khoảng cách giữa các toa trên ray. align-items điều khiển chiều cao từng toa so với container. Khi flex-direction: column, hai thuộc tính này đảo vai trò — justify-content canh dọc, align-items canh ngang. Nhớ hình ảnh đoàn tàu, bạn sẽ không bao giờ nhầm.
#Phần 4 — Kích thước giả định (Hypothetical Size)
Trước khi nói về grow/shrink, bạn cần hiểu một khái niệm nền tảng: trong Flexbox, width (hoặc height nếu flex-direction: column) là một gợi ý, không phải ràng buộc cứng.
Thuật toán Flexbox hoạt động theo ba bước:
- Tính kích thước giả định (hypothetical size) của mỗi item dựa trên
flex-basis, rồiwidth/height, rồi nội dung. - So sánh tổng kích thước giả định với không gian thực tế của container.
- Nếu dư → dùng
flex-growđể phân phối. Nếu thiếu → dùngflex-shrinkđể co lại.
Điều này có nghĩa là bạn có thể set width: 200px cho một item, nhưng nếu container quá nhỏ, Flexbox vẫn sẽ co item đó lại (trừ khi bị chặn bởi min-width). Và nếu container quá lớn, item vẫn giữ 200px trừ khi bạn set flex-grow.
.item {
width: 200px; /* "tôi muốn 200px" — nhưng Flexbox có thể bỏ qua */
flex-shrink: 1; /* cho phép co lại nếu thiếu chỗ (mặc định) */
}Đây là lý do nhiều dev hoang mang: "tôi đã set width rồi mà sao nó vẫn tràn?" hoặc "tôi đã set width rồi mà sao nó vẫn co lại?". Câu trả lời luôn là: Flexbox ưu tiên phân phối không gian, không ưu tiên width.
#Phần 5 — Grow / Shrink / Basis
Ba thuộc tính này tạo nên cốt lõi của thuật toán phân phối không gian trong Flexbox.
#flex-basis: kích thước "mong muốn" dọc theo ray
flex-basis là cách bạn nói "trước khi phân phối, hãy cho tôi X pixel dọc theo đường ray". Nó giống width nhưng dọc theo trục chính (nếu flex-direction: row thì giống width, nếu column thì giống height).
.item {
flex-basis: 200px; /* tôi muốn 200px trên trục chính */
}Thứ tự ưu tiên: flex-basis > width/height > nội dung. Nếu bạn set flex-basis, Flexbox sẽ dùng nó thay vì width.
Thử kéo slider trong demo dưới đây để thay đổi flex-basis của từng toa tàu. Chú ý cách không gian dư được phân phối khi tổng basis nhỏ hơn container.
#flex-grow: phân phối không gian dư
Sau khi tính xong kích thước giả định, nếu tổng nhỏ hơn container, phần dư được chia theo tỷ lệ flex-grow.
.item-a { flex-grow: 1; } /* nhận 1 phần */
.item-b { flex-grow: 2; } /* nhận 2 phần — gấp đôi item-a */
.item-c { flex-grow: 1; } /* nhận 1 phần */Mặc định flex-grow: 0 — nghĩa là item không lấy thêm không gian dư. Nếu chỉ có một item set flex-grow: 1, nó sẽ chiếm toàn bộ không gian dư.
#flex-shrink: phân phối thiếu hụt
Đây là "mặt còn lại" của flex-grow. Khi tổng kích thước giả định lớn hơn container, mỗi item co lại theo tỷ lệ flex-shrink.
.item-a { flex-shrink: 1; } /* co lại 1 phần (mặc định) */
.item-b { flex-shrink: 0; } /* KHÔNG co lại — giữ nguyên kích thước */Lưu ý: thuật toán shrink phức tạp hơn grow một chút. Nó nhân flex-shrink với flex-basis để tính phần co thực tế. Item nào basis lớn hơn sẽ co nhiều hơn (tuyệt đối), dù cùng flex-shrink: 1.
Thử điều chỉnh flex-grow, flex-shrink và kéo thanh container width trong demo dưới đây. Chú ý cách không gian dư/thiếu được phân phối giữa các toa.
Takeaway: flex-basis là kích thước mong muốn, flex-grow là "tôi muốn thêm nếu dư", flex-shrink là "tôi chấp nhận co nếu thiếu". Ba thuộc tính này luôn hoạt động cùng nhau — đừng tách rời chúng.
#Phần 6 — Minimum Size Gotcha (Phần quan trọng nhất cho dev thực chiến)
Đây là gotcha phổ biến nhất trong Flexbox, và cũng là thứ mà nhiều senior dev vẫn bị dính.
Vấn đề: bạn có một flex container, bên trong có input hoặc text dài. Bạn set flex-shrink: 1, nhưng item không chịu co lại. Text tràn ra ngoài, hoặc input đẩy layout vỡ.
/* Setup trông rất đúng */
.container {
display: flex;
}
.text-block {
flex-shrink: 1; /* cho phép co — nhưng không co! */
}Tại sao? Flexbox có một quy tắc ngầm: mỗi flex item có minimum size dựa trên nội dung. Với text, minimum size bằng chiều dài của từ dài nhất (hoặc intrinsic minimum width). Flexbox sẽ không co item nhỏ hơn minimum size này, bất kể flex-shrink là bao nhiêu.
Đây là quy tắc bảo vệ — nó ngăn text bị cắt cụt đến mức không đọc được. Nhưng trong nhiều trường hợp (sidebar, card, responsive layout), nó lại phá layout.
Giải pháp: ghi đè minimum size bằng min-width: 0 (hoặc min-height: 0 cho column).
.text-block {
flex-shrink: 1;
min-width: 0; /* cho phép co nhỏ hơn minimum intrinsic size */
}Thử toggle min-width: 0 trong demo dưới đây. Bạn sẽ thấy text block chuyển từ "không chịu co" sang "co lại bình thường".
Takeaway: nếu một flex item không co lại dù bạn đã set flex-shrink: 1, hãy kiểm tra min-width. Đây là nguyên nhân số một khiến Flex layout bị vỡ trong thực tế — đặc biệt với text, image, và input elements.
#Phần 7 — Gap & Auto Margins
#gap: khoảng cách giữa các toa
Trước khi có gap, dev phải dùng margin trên từng item rồi trừ margin đầu/cuối — rất dễ sai. gap giải quyết gọn gàng:
.container {
display: flex;
gap: 16px; /* khoảng cách giữa tất cả items */
gap: 16px 24px; /* row-gap: 16px, column-gap: 24px */
}gap chỉ tạo khoảng cách giữa các item, không thêm space ở đầu hay cuối. Đây là hành vi chính xác trong hầu hết trường hợp.
#margin: auto — "ăn" không gian còn lại
Một trick mạnh mẽ: margin: auto trên flex item sẽ hấp thụ toàn bộ không gian còn lại theo hướng tương ứng.
.navbar {
display: flex;
align-items: center;
}
.logo { }
.spacer {
margin-left: auto; /* đẩys mọi thứ sau nó sang phải */
}
.nav-links { }Đây là pattern kinh điển cho navbar: logo bên trái, nav links bên phải, khoảng trắng tự động ở giữa. Không cần justify-content: space-between và không cần thêm spacer element.
/* Hoặc căn giữa một item duy nhất */
.centered-item {
margin: auto; /* hấp thụ space cả hai phía */
}#Phần 8 — Wrapping: Khi đường ray không đủ chỗ
Mặc định, tất cả flex items cố gắng nằm trên một đường ray duy nhất — bất kể tổng kích thước có vượt container hay không. flex-wrap thay đổi hành vi này:
.container {
display: flex;
flex-wrap: nowrap; /* mặc định — tất cả trên một ray */
flex-wrap: wrap; /* các toa tự xuống hàng khi hết chỗ */
}Khi flex-wrap: wrap, mỗi "hàng" (line) trở thành một sub-flex-container riêng biệt. Không gian còn lại trên mỗi hàng được phân phối độc lập.
Điều quan trọng: khi có nhiều hàng, một thuộc tính mới xuất hiện — align-content. Nó quyết định cách các hàng phân bố dọc theo trục phụ.
.container {
display: flex;
flex-wrap: wrap;
align-items: flex-start; /* canh item trong MỖI hàng */
align-content: center; /* canh TẤT CẢ các hàng trong container */
}Sự khác biệt:
align-items: canh item so với hàng của nó (tương tự single-line).align-content: canh các hàng so với toàn bộ container. Chỉ có tác dụng khi có nhiều hàng.
Nếu bạn quên flex-wrap: wrap, align-content sẽ không có tác dụng gì — vì chỉ có một hàng duy nhất.
Thử toggle wrap và so sánh align-items vs align-content trong demo dưới đây.
Takeaway: flex-wrap: wrap tạo ra nhiều đường ray song song. align-items canh toa trong mỗi ray, align-content canh các ray so với nhà ga (container). Chỉ dùng align-content khi có nhiều hàng.
#Phần 9 — Tổng kết & Ví dụ thực chiến
Bạn đã đi qua toàn bộ mental model của Flexbox. Giờ hãy ráp lại thành một ví dụ thực tế: responsive form không cần media query.
.form-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-start; /* label và input căn trên cùng */
}
.form-label {
flex-basis: 120px;
flex-shrink: 0; /* label không co lại */
}
.form-input {
flex-basis: 200px;
flex-grow: 1; /* input chiếm hết space còn lại */
min-width: 0; /* ← gotcha ở Phần 6 */
}Khi màn hình rộng: label 120px + input co dãn theo container. Khi màn hình hẹp: flex-wrap: wrap đẩy input xuống hàng mới, flex-grow: 1 cho phép input chiếm toàn bộ chiều rộng. Không một dòng media query nào.
#Tóm tắt mental model
- Đường ray: luôn xác định trục chính trước.
flex-directionquyết định mọi thứ. - Phân phối không gian:
justify-contentdọc ray,align-itemsvuông góc ray. Nhớ ma trận 2×2. - Kích thước là gợi ý: Flexbox ưu tiên phân phối, không ưu tiên width.
flex-basis→width→ nội dung. - Grow & Shrink: hai mặt của cùng một đồng xu. Grow phân phối dư, shrink phân phối thiếu. Luôn nhớ minimum size.
- Min-width gotcha: nếu item không co → kiểm tra
min-width. Đây là lỗi phổ biến nhất. - Gap & margin auto:
gapcho khoảng cách đều,margin: autocho khoảng cách linh hoạt. - Wrap tạo sub-container: mỗi hàng là flex container riêng.
align-contentchỉ hoạt động khi có nhiều hàng.
Flexbox không khó — nó chỉ yêu cầu bạn hiểu đúng mental model. Khi bạn nghĩ về đoàn tàu trên đường ray, mọi thuộc tính đều có vị trí rõ ràng trong đầu.