Một thanh lọc có ba hàng tag khá đầy nhưng dòng cuối chỉ còn đúng một nút không bị “hỏng”, dù vậy khối giao diện vẫn trông thiếu cân đối. CSS flex-wrap balance nhắm thẳng vào chuyện đó. Thay vì nhét item theo kiểu gặp đâu xếp đó, trình duyệt thử chia lại để độ dài các flex line gần nhau hơn.

Mình đã dựng một fixture gồm 12 nhãn tiếng Việt, chạy thật trên Chrome 151, đo ở ba viewport và so với Grid. Kết quả khá đẹp ở 768 và 1.280 px, nhưng 320 px gần như không đổi. Đây là progressive enhancement đáng thử cho chip, chứ không phải nút “làm UI đẹp tự động”.

Dòng cuối Flexbox lệch khó chịu ở đâu?

flex-wrap: wrap tạo dòng mới khi item tiếp theo không còn chỗ. Cách này dễ hiểu, nhanh và đúng với rất nhiều giao diện. Vấn đề xuất hiện khi độ dài nhãn không đều: “CSS” chỉ chiếm một đoạn ngắn, còn “Progressive enhancement” dài gần gấp bốn lần. Việc xếp tham lam có thể để dòng đầu 5 item, dòng giữa 4 và dòng cuối 3.

Không có lỗi chức năng nào ở đây. Người dùng vẫn đọc và bấm được. Tuy nhiên với filter chip, danh mục hoặc cụm nút phụ, phần rag — mép kết thúc lởm chởm của từng hàng — làm khối giao diện trông thiếu chủ ý. Mẹo đặt width cho từng nút hoặc viết media query theo số lượng thường chỉ đẹp với đúng bộ chữ mẫu; đổi sang nhãn tiếng Việt dài hơn là tới công chuyện.

flex-wrap: balance thực sự thay đổi gì?

Bản CSS Flexible Box Layout Module Level 2 ngày 15/05/2026 định nghĩa balance là cố làm độ dài các flex line giống nhau nhất có thể. Giá trị này tạo container nhiều dòng; nếu không ghi hướng wrap thì nó cư xử như wrap. Nó cũng có thể đi cùng wrap-reverse, dù mình không thấy lý do tốt để dùng hướng đảo trong dàn tag đọc tuần tự.

Điểm quan trọng là trình duyệt cân độ dài dòng, không hứa số item mỗi dòng luôn bằng nhau. Ở fixture của mình, 4–4–4 xảy ra tại 768 px vì độ dài nhãn tình cờ phù hợp. Ở 1.280 px, kết quả là 7–5 thay vì 8–4. Đây là kết quả hợp lý hơn về thị giác, nhưng không phải một grid ảo.

Chrome 150 release notes ghi tính năng vào stable ngày 30/06/2026 trên Android, ChromeOS, Linux, macOS và Windows. Chrome 151 trên máy mình trả true cho CSS.supports('flex-wrap', 'balance'), và computed value đúng là balance.

Fixture tiếng Việt được dựng ra sao?

Môi trường kiểm tra là Windows 11, Chrome 151.0.7922.76 headless. Mình dùng cùng một thứ tự 12 nhãn: Thiết kế UI, CSS, Laravel thực chiến, Tối ưu hiệu năng, Khả năng tiếp cận, Responsive, Màu sắc, Typography tiếng Việt, Design system, Kiểm thử bàn phím, Frontend và Progressive enhancement.

Mỗi chip là button thật ở biến thể balance, cao tối thiểu 44 CSS px, padding ngang 14 px, gap 10 px và font Arial 15 px. Ba panel dùng cùng dữ liệu: wrap thường, balance qua @supports, và Grid tham chiếu. Script nhóm item theo tọa độ top để đếm số lượng từng dòng; không nhìn ảnh rồi đoán.

So sánh wrap thường 5 4 3, balance 4 4 4 và Grid ở viewport 768 px
Cùng 12 nhãn và DOM tại 768 px: wrap thường là 5–4–3, balance là 4–4–4.

Ảnh trên là screenshot tự chụp từ fixture 768 px. Các bạn có thể thấy wrap dùng gần hết dòng đầu rồi ngắn dần, còn balance chấp nhận xuống dòng sớm để ba hàng đều có bốn item. Grid thẳng cột hơn nhưng làm chip giãn bằng nhau, tức là thay đổi hẳn ngôn ngữ thị giác.

Kết quả ở 320, 768 và 1.280 px

ViewportWrap thườngBalanceGrid tham chiếu
320 px10 dòng, chủ yếu 1 itemGiống wrap12 dòng, mỗi dòng 1 item
768 px5–4–34–4–44–4–4
1.280 px8–47–58–4

Ở 320 px, chiều rộng hữu dụng của panel chỉ còn 241 px. Nhiều nhãn vốn đã rộng 150–219 px, nên thuật toán gần như không có lựa chọn để hoán đổi điểm ngắt. Balance không làm chữ nhỏ đi, không ép item co và cũng không tự chia nhãn dài thành hai cột. Việc “không cải thiện” ở màn hình này lại là hành vi mình muốn: nội dung vẫn nguyên và không tràn.

Tại 768 px, chênh lệch rõ nhất. Wrap tạo ba dòng dài 691, 563 và 504 px; balance tạo 520, 587 và 652 px. Số item cân hơn, dù tổng chiều dài từng dòng chưa hoàn hảo. Tại 1.280 px, wrap dùng 1.117 px ở dòng đầu và 652 px ở dòng sau; balance thu khoảng cách xuống còn 918 và 850 px.

So sánh dàn tag wrap balance và Grid ở viewport 1280 px
Tại 1.280 px, balance thu rag từ 8–4 thành 7–5 mà không kéo chip thành cột bằng nhau như Grid.

Chỗ cân hơn nhưng chưa chắc tốt hơn

Thứ nhất, balance có thể đẩy một item xuống sớm. Nếu chiều cao component là phần cứng của layout, một điểm ngắt mới có thể làm header cao thêm. Thứ hai, khi label đổi sau khi dịch hoặc font web hoàn tất tải, trình duyệt sẽ cân lại. Trong test đổi mô phỏng từ Times New Roman sang Arial ở 768 px, cả hai đều giữ 4–4–4 và không tràn; đó chỉ là một bộ nhãn, không phải bảo đảm cho mọi font.

Thứ ba, đừng biến cân bằng thành mục tiêu lớn hơn khả năng đọc. Nút “Khả năng tiếp cận” không nên bị ép còn 32 px cao chỉ để hàng ngay ngắn. Fixture giữ min-height 44 px, cho phép nhãn dài ngắt từ và kiểm tra ở không gian layout tương đương zoom 200%: màn hình vật lý 768 px, viewport CSS hiệu dụng 384 px. Trang không tràn ngang, hit target vẫn 44 CSS px.

Mình còn chạy một benchmark nhỏ bằng cách đổi qua lại giữa nowrap và hai giá trị wrap, buộc đọc offsetHeight 120 lần. Với 12 item, median là 8,2 ms cho balance và 7,9 ms cho wrap; với 200 item là 146,9 và 156,8 ms. Chênh lệch đổi chiều và quá nhỏ để kết luận balance nhanh hơn. Điều đáng giữ là trên fixture này mình chưa thấy overhead có ý nghĩa; đây không phải benchmark engine hay số liệu production.

Fallback không làm Firefox và Safari vỡ UI

Mình bắt đầu từ wrap, rồi chỉ nâng cấp khi browser hiểu giá trị mới. Nếu không hỗ trợ, cả khối @supports bị bỏ qua và layout quay về hành vi đã chạy nhiều năm:

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
}

@supports (flex-wrap: balance) {
  .tag-list {
    flex-wrap: balance;
  }
}

Mình tắt rule balance ngay trong fixture: đủ 12 nhãn, DOM order giữ nguyên và không có overflow. Đây cũng là cách an toàn hơn so với phụ thuộc vào user-agent string. hướng dẫn wrapping của MDN, sửa lần cuối 07/11/2025, vẫn mô tả nền tảng wrap truyền thống chứ chưa có mục riêng cho balance. Vì vậy mình không ghi Firefox hay Safari “đã hỗ trợ” chỉ dựa vào phỏng đoán.

Trang WPT cho flexbox balance là nơi nên xem lại khi browser matrix thay đổi. Mình không có Firefox hoặc Safari trên máy Windows lần này; dữ liệu WPT và bug đồng bộ WPT của Mozilla chỉ được dùng để nhắc rằng triển khai còn chuyển động, không thay cho test trực tiếp.

Keyboard, RTL và zoom có đổi thứ tự đọc không?

balance đổi điểm xuống dòng, không đổi DOM. Mình gửi phím Tab thật qua Chrome cho 12 button: thứ tự focus khớp hoàn toàn thứ tự source từ “Thiết kế UI” đến “Progressive enhancement”. Khi chuyển document sang RTL, mảng DOM vẫn giữ nguyên và khối không tràn. Nhãn cực dài cũng xuống dòng bên trong chip thay vì kéo rộng trang.

Fixture dàn tag tại không gian layout tương đương zoom 200 phần trăm có vòng focus
Viewport CSS hiệu dụng 384 px trên màn hình 768 px: không tràn ngang, hit target vẫn 44 CSS px và focus còn nhìn rõ.

Ảnh kiểm tra không gian layout 200% còn giữ vòng focus 4 px ở button cuối. Chỗ này giống bài mình đo focus indicator WCAG 2.2: bố cục đẹp mà làm mất dấu bàn phím thì coi như thua. Các bạn cũng không nên dùng order để “sửa” từng dòng, vì visual order khi đó có thể tách khỏi source và speech order.

Khi nào nên quay về CSS Grid?

Nếu component là một dòng chip có chiều rộng tự nhiên, Flexbox hợp với ý nghĩa một chiều. Balance chỉ giúp chọn điểm ngắt dễ nhìn hơn. Nếu mục tiêu là các card thẳng cả hàng lẫn cột, có cột bằng nhau và nút cùng vị trí, Grid mới đúng bài toán. Screenshot cho thấy Grid kéo “CSS” rộng ngang “Laravel thực chiến”; với filter chip, khoảng trắng đó trông hơi cứng.

Mình sẽ không thay toàn bộ layout đang ổn. Với form tự co giãn, cách triển khai an toàn cũng giống bài CSS field-sizing: nền tảng cũ chạy trước, tính năng mới chỉ cải thiện. Nếu cần đánh giá property mới trên Chrome, bài CSS text-fit cho tiêu đề tiếng Việt có cùng cách đo viewport nhưng giải quyết typography, không phải line breaking của flex item. Còn component popup có tương tác phức tạp thì nên xem lại checklist fallback trong bài dialog CSS mới.

Balance phù hợp ở đâu?

flex-wrap: balance phù hợp với nhóm filter chip, tag chủ đề và các nút phụ có số lượng vừa phải, nơi mép hàng lởm chởm thực sự làm giao diện kém gọn. Nó không thay Grid, không phù hợp để cố cân danh sách 200 item luôn thay đổi, và vẫn cần fallback khi hỗ trợ đa trình duyệt chưa đồng đều.

Kết quả đáng nhớ nhất là 5–4–3 thành 4–4–4 ở 768 px và 8–4 thành 7–5 ở 1.280 px, trong khi 320 px không được “cứu”. Đúng chỗ thì một dòng CSS làm UI bớt lệch; sai kỳ vọng thì nó chỉ chuyển item sang hàng khác thôi. Anh em có bộ tag tiếng Việt oái oăm hơn cứ thử với fixture riêng trước khi đưa vào design system.

Nguồn kiểm chứng: CSS Flexible Box Layout Module Level 2 — Editor’s Draft 15/05/2026; Chrome 150 release notes — stable 30/06/2026; MDN Mastering wrapping of flex items — cập nhật 07/11/2025; WPT flexbox balance và Mozilla Bugzilla 2051495. Tất cả được kiểm tra lại ngày 12/08/2026. Phần số đo, screenshot và nhận định là từ fixture local của mình trong lần viết bài này.