Bài này nói về layout CSS mức senior cho phỏng vấn: Flexbox, Grid, khi nào kết hợp chúng, chiến lược responsive, stacking, kích thước intrinsic, và công cụ hiện đại (subgrid, :has(), @layer). Không phải cheat sheet thuộc tính. Người phỏng vấn xem bạn giải thích được modal dưới sticky header, flex child không chịu co, và nhảy 40px khi ảnh load.
Đọc 3.1–3.3 trước (Flex, Grid, ai chịu trách nhiệm trang). Rồi 3.4–3.6 cho container queries, stacking, và min-width: 0. 3.7 là phần phụ có ghi chú support. Trả lời bằng một quyết định, một ràng buộc, một kịch bản lỗi, và cách đo (CLS, overlay DevTools, test case rút gọn). Vue v-if và v-for xuất hiện vì chúng đổi sibling nào là :last-child.
Tóm tắt: Flexbox chia một trục (main) và canh trục kia (cross). flex: 1 là 1 1 0%: nó không thắng min-width: auto mặc định (tối thiểu nội dung). Để min-width: 0 trên child phải co. Dùng gap, đừng dùng margin :last-child.
Vì sao quan trọng: Toolbar (logo trái, nav giữa, action phải) tràn trên màn nhỏ thay vì co. Hoặc hàng card mà một SKU dài kéo cả hàng. Họ đang đào min-width: auto, gap khác margin, và wrapping, không phải trivia justify-content.
- Cách hoạt động:
display: flextạo flex formatting context. Trục main theoflex-direction.flex: 1nghĩa là grow, shrink, và bắt đầu từ basis 0. Item vẫn từ chối co dưới tối thiểu nội dung (min-width: auto).min-width: 0(hoặcoverflow: hiddennếu bạn cũng clip) cho phép co. - Chốt action bên phải bằng
margin-inline-start: auto, không phảispace-betweentrên hai item rồiv-ifthêm child thứ ba.gapbỏ qua childdisplay: none; margin:last-childthì không.flex-wraptrên CTA toolbar có thể đẩy nó xuống dưới fold: đó là bug product, không phải thắng CSS. - CSS
orderlà mùi a11y trongv-forVue: thứ tự nhìn ≠ thứ tự tab ≠ thứ tự screen reader. Tương đương Vue: cùng câu chuyệnmin-width: 0trên flex child với tên file interpolate dài.
.toolbar {
display: flex;
align-items: center;
gap: 0.75rem; /* không dùng margin :last-child: v-if Vue để lại gap cuối */
}
.toolbar__nav {
flex: 1 1 auto; /* grow, shrink, basis theo nội dung */
min-width: 0; /* cho co dưới min nội dung (SKU / tên file dài) */
overflow: auto;
}
.toolbar__actions {
margin-inline-start: auto; /* chốt bên phải; vẫn đúng với child v-if thứ ba */
flex: 0 0 auto;
}
/* một mình flex: 1 không chặn từ nowrap tràn. min-width: 0 mới chặn. */Điều gì xảy ra: Nav lấy khoảng còn lại và được phép co dưới tối thiểu nội dung của SKU, nên nó cuộn thay vì kéo toolbar. Action ở lại bên phải dù badge v-if xuất hiện. gap không để lỗ khi child là display: none.
| Tình huống | Điều gì xảy ra | Nên ship |
|---|---|---|
flex: 1 + từ dài | Không co dưới min-width: auto | min-width: 0 (hoặc clip) |
space-between + v-if thứ ba | Khoảng cách trông ngẫu nhiên | margin-inline-start: auto |
Margin :last-child + filter Vue | Gap cuối | gap |
flex-basis: auto card đều | Padding/border phá đều | flex: 1 1 0 / flex-basis: 0 |
Góc nhìn senior
- Trục main là trục
flex-directionchỉ tới. Cross là canh (align-items). Nói cả hai từ. flex: 1không phải giấy phép co.min-width: autolà tối thiểu nội dung. Đó là câu phỏng vấn.gapkhác margin quan trọng trong Vue vìv-ifgỡ box mà không gỡ lỗ:last-child.ordervà wrap chỉ đổi thứ tự nhìn đánh nhau với cây accessibility. Giữ thứ tự DOM thật.- Flex cột với
overflow: autovẫn cầnmin-height: 0: cùng câu chuyện tối thiểu nội dung trên trục kia.
Lỗi hay gặp
- Triệu chứng: tên file dài tràn child
flex: 1. Nguyên nhân:min-width: auto. Cách xử lý:min-width: 0.- Triệu chứng: gap cuối sau filter Vue. Nguyên nhân: margin
:last-child. Cách xử lý:gap.- Triệu chứng: CTA wrap xuống dưới fold. Nguyên nhân:
flex-wrap: wraptrên toolbar. Cách xử lý: overflow nav, không phải CTA.- Triệu chứng: thứ tự tab nhảy. Nguyên nhân: CSS
ordertrongv-for. Cách xử lý: reorder trong array.
Nhà tuyển dụng hay hỏi gì
Toolbar tràn trên màn hình nhỏ, hoặc một SKU dài kéo cả hàng card. Họ muốn min-width: auto, gap khác margin, và wrapping, không phải trivia justify-content.
Trả lời khi phỏng vấn
Mình Flex một trục, kiểu nav, hàng form, cụm button, và để min-width 0 trên child phải co. flex 1 không thắng min-width auto; mặc định đó là tối thiểu nội dung, nên SKU dài vẫn tràn. Mình dùng gap thay margin last-child vì v-if Vue sẽ để lại gap cuối. Mình chốt action bên phải bằng margin-inline-start auto để child thứ ba không làm space-between nhảy lung tung. Mình wrap toolbar chỉ khi product đồng ý CTA được rơi xuống. CSS order là mùi a11y; mình reorder array. Trục kia là 3.2.
Nhớ nhanh
- Flex là một trục.
flex: 1không thắngmin-width: auto. min-width: 0trên child phải co.gapchịu đượcv-ifVue. Margin:last-childthì không.- Chốt action bên phải bằng
margin-inline-start: auto. Tránh CSSorder.
Câu hỏi nối tiếp thường gặp
- Vì sao flex 1 không chặn tên file dài? min-width auto là tối thiểu nội dung. Gán min-width 0.
- gap khác margin trên list Vue đã filter? gap. Margin last-child để lại lỗ.
- Khi nào flex-wrap là bug product? CTA toolbar rơi xuống dưới fold.
- flex-basis 0 khác auto cho card đều? 0. Padding và border phá sự đều của auto.
Tóm tắt: Grid phụ trách hai trục cùng lúc: track (fr, minmax), area đặt tên, và auto-fit khác auto-fill. 1fr là minmax(auto, 1fr): cùng bẫy tối thiểu nội dung như Flex. Để minmax(0, 1fr) trên mọi track phải co.
Vì sao quan trọng: Dashboard (sidebar, top bar, main, inspector tùy chọn), hoặc lưới card phải lấp hàng mà không media query mỗi breakpoint. Họ muốn minmax, fr, area đặt tên, và câu chuyện overflow, không phải 1fr 1fr 1fr.
- Cách hoạt động:
grid-template-columnsvàgrid-template-rowstạo track.frchia khoảng còn sau min-content.minmax(0, 1fr)cho track co.grid-template-areasđặt tên giúp shell dễ đọc lúc review: người mới chuyển inspector trong một lần sửa. repeat(auto-fit, minmax(min(100%, 16rem), 1fr))lấp đầy gallery mà không cần breakpoint theo số card.auto-fitgộp track trống;auto-fillgiữ chúng.min(100%, 16rem)cứu bạn ở 320px chỗminmax(200px, 1fr)tràn.- Ẩn inspector trong Vue bằng cách đổi area / column, không phải
display: nonetrên một area để lại track trống.position: stickyfail nếu grid item (hoặcmain) là ancestor overflow. Tương đương Vue: childv-forchưa được đặt vào area sẽ tạo implicit row và kéo shell.
.app {
display: grid;
grid-template-columns: minmax(0, 16rem) minmax(0, 1fr) minmax(0, 20rem);
grid-template-rows: auto minmax(0, 1fr);
grid-template-areas:
'nav top top'
'nav main inspector';
min-height: 100dvh;
}
@media (max-width: 64rem) {
.app {
grid-template-columns: minmax(0, 1fr);
grid-template-areas: 'top' 'main'; /* inspector bỏ: không để track trống */
}
}
.gallery { grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }Điều gì xảy ra: Area đặt tên giữ shell sửa được. minmax(0, 1fr) cho main co để trang không phình vì table nowrap. Gallery wrap theo chiều rộng card, không theo list breakpoint. Ở 64rem track inspector biến mất, không phải trống.
| Pattern | Dùng khi | Bẫy |
|---|---|---|
| Area đặt tên | Shell app | Ẩn cột Vue sai cách để lại track |
1fr | Track co giãn | Nghĩa là minmax(auto, 1fr): nowrap tràn |
auto-fit + min(100%, 16rem) | Gallery không cần breakpoint theo số | minmax(200px, 1fr) tràn ở 320px |
auto-fill | Giữ track trống (thư viện widget) | Trông thưa nếu bạn muốn gộp |
Góc nhìn senior
- Đặt tên track. Người mới chuyển được inspector mà không đếm số line.
1frlà cùng bẫy min-content như Flex.minmax(0, 1fr)là giấy phép co.auto-fitkhácauto-fill: gộp khác giữ track trống. Thư viện widget thường muốn fill.grid-auto-flow: denselấp lỗ và phá thứ tự nhìn so với thứ tự tab. Cùng mùi a11y vớiorder.- Sticky trong
mainfail khimainoverflow. Nêu rõ ancestor overflow.
Lỗi hay gặp
- Triệu chứng: cột
1frtràn table nowrap. Nguyên nhân:minmax(auto, 1fr). Cách xử lý:minmax(0, 1fr).- Triệu chứng: cột inspector trống sau
v-if. Nguyên nhân: area vẫn được giữ. Cách xử lý: viết lạigrid-template-areas.- Triệu chứng: gallery tràn ở 320px. Nguyên nhân:
minmax(200px, 1fr). Cách xử lý:min(100%, 16rem).- Triệu chứng: sticky header không dính. Nguyên nhân: overflow trên grid item. Cách xử lý: đưa overflow vào scroller bên trong.
Nhà tuyển dụng hay hỏi gì
Shell dashboard kèm lưới card, không được thêm breakpoint theo số card. Họ muốn minmax, fr, area đặt tên, và câu chuyện overflow, không phải ba cột 1fr.
Trả lời khi phỏng vấn
Mình Grid hai trục cùng lúc: chrome trang, hàng label và field form, gallery card. Shell dùng grid-template-areas đặt tên để người mới chuyển inspector trong một lần sửa; gallery dùng auto-fit kèm minmax min 100% 16rem 1fr để mình khỏi viết breakpoint theo số card. 1fr là minmax auto 1fr, nên table nowrap vẫn tràn đến khi mình viết minmax 0 1fr. Mình ẩn cột Vue bằng cách viết lại area, không phải display none. Sticky fail trên ancestor overflow. auto-fill giữ track trống cho thư viện widget; auto-fit thì gộp.
Nhớ nhanh
- Grid phụ trách hai trục. Đặt tên area của shell.
1frlàminmax(auto, 1fr). Dùngminmax(0, 1fr)để co.auto-fitgộp track trống;auto-fillgiữ chúng.min(100%, 16rem)trongminmaxcứu overflow ở 320px.
Câu hỏi nối tiếp thường gặp
- Vì sao cột 1fr tràn nowrap? Vì 1fr là minmax auto 1fr. Viết minmax 0 1fr.
- auto-fit khác auto-fill cho thư viện widget? auto-fill: giữ track trống làm slot.
- Ẩn inspector trong Vue? Viết lại area/column. display none để lại track.
- min 100% 16rem cứu gì ở 320px? Min 16rem sẽ tràn viewport.
Tóm tắt: Grid cho trang (track đặt tên, hai trục). Flex cho component (một trục: nav, cụm button, input+addon). Nâng hàng Flex lên Grid khi các hàng sibling phải canh chung. “1D khác 2D” là câu mid. Câu thật là ai phụ trách chrome.
Vì sao quan trọng: Người phỏng vấn hỏi cái nào phụ trách trang, chúng ghép thế nào, và bug nào riêng mỗi cái (overflow lúc co, stacking, gap). Gallery 2D bằng Flex wrap thì lệch hàng. Toolbar bằng Grid là bốn tên line cho margin-left: auto.
- Cách hoạt động: Grid cho trang: hàng và cột, area đặt tên, track tường minh. Trong Vue thường là
AppLayout.vuelàm Grid, còn trang chỉ Flex nội dung. Flex cho component: nav, cụm button, input+addon, icon+copy empty-state. - Nâng hàng Flex lên Grid khi các hàng sibling phải dùng chung track (label / field / help form). Flex lồng Flex để giả alignment đó là bẫy. Sticky footer thật thà là
1frmain trong Grid;align-items: centertrên trang Flex ngắn phá “footer ở đáy”. - Bạn không bỏ cả hai cho
float, trừ ảnh wrap quanh chữ trong bài viết. Content-out (Flex: size từ item) khác layout-in (Grid: size từ track) là câu kia họ muốn nghe.
/* Ô dashboard: Grid phụ trách trang, Flex phụ trách panel. */
.main {
grid-area: main;
min-height: 0; /* track Grid phải co */
display: flex;
flex-direction: column;
}
.main__scroll {
flex: 1 1 auto;
min-height: 0; /* flex child phải co, không thì trang cuộn */
overflow: auto;
}
/* Nâng hàng form lên Grid khi label/field/help phải dùng chung track */
.form { display: grid; grid-template-columns: 8rem minmax(0, 1fr) 12rem; gap: 0.5rem 1rem; }Điều gì xảy ra: Grid của trang co main. Bên trong, Flex cột lấp chiều cao và scroller nhận min-height: 0 để hộp trong cuộn, không phải cửa sổ. Grid form canh help xuyên hàng. Flex wrap thì không.
| Tình huống | Flex | Grid |
|---|---|---|
| Chrome trang | Ổn với một cột; mong manh khi có hai | Area đặt tên: phụ trách trang |
| Toolbar | Phân phối một trục tự nhiên | Tên line thừa |
| Gallery card | Wrap lệch hàng | Track canh cả hai trục |
| Sticky footer | Center trên trang ngắn thì hỏng | 1fr main là thật thà |
Góc nhìn senior
- Ai phụ trách chrome mới là câu senior. 1D khác 2D là câu mid bạn nói trước, rồi nâng.
min-height: 0thuộc track Grid và Flex child trong Grid-of-Flex. Thiếu một, trang cuộn.- Content-out khác layout-in: Flex size từ item; Grid size từ track. Gallery muốn track.
- Hàng form 1D vẫn nên Grid khi hàng sau phải dùng chung cột label.
- Vue:
AppLayout.vueGrid, trang Flex. Đừng để mọi view dựng lại shell.
Lỗi hay gặp
- Triệu chứng: gallery lệch hàng. Nguyên nhân: Flex wrap như 2D. Cách xử lý: track Grid.
- Triệu chứng: trang cuộn, panel trong thì không. Nguyên nhân: thiếu
min-height: 0trên track hoặc child. Cách xử lý: cả hai.- Triệu chứng: bốn line Grid cho button đậu phải. Nguyên nhân: Grid trên toolbar. Cách xử lý: Flex +
margin-inline-start: auto.- Triệu chứng: Flex lồng giả dùng chung cột. Nguyên nhân: sợ Grid. Cách xử lý: nâng form.
Nhà tuyển dụng hay hỏi gì
“Flex là 1D, Grid là 2D” là câu mid. Senior được hỏi cái nào phụ trách trang, chúng ghép thế nào, và bug nào riêng mỗi cái.
Trả lời khi phỏng vấn
Mình Grid trang và Flex component. 1D khác 2D là câu mid. Câu thật là ai phụ trách chrome. Mình nâng hàng Flex lên Grid khi các hàng sibling phải dùng chung track, như label, field, và help form. Mình không Flex gallery 2D chỉ vì wrap tồn tại: cột lệch hàng và item thứ hai không canh. Sticky footer là main 1fr, không phải align-items center trên trang ngắn. min-height 0 thuộc track Grid và Flex child. Content-out khác layout-in là câu kia. Mình không float trừ ảnh wrap trong bài viết.
Nhớ nhanh
- Grid cho trang (track đặt tên). Flex cho component (một trục).
- Nâng hàng lên Grid khi các hàng sibling phải canh chung.
min-height: 0trên track Grid và Flex child.- Content-out (Flex) khác layout-in (Grid). Gallery muốn track.
Câu hỏi nối tiếp thường gặp
- Layout 1D bạn vẫn Grid? Label/field/help form: hàng sibling phải dùng chung track.
- Layout 2D bạn vẫn Flex? List chip wrap mà hàng không cần canh.
- min-height 0 thuộc đâu trong Grid-of-Flex? Track và Flex child đang cuộn.
- Vì sao sticky footer dễ hơn trong Grid? Main 1fr. Flex center trên trang ngắn thì cản bạn.
Tóm tắt: Media query mobile-first phụ trách chrome trang. Container query phụ trách widget. Type fluid là clamp bằng rem để zoom 200% vẫn chạy. Chừa kích thước trước load (aspect-ratio) để CLS trong ngân sách. Chiều cao viewport là 100dvh, không phải 100vh, trên iOS.
Vì sao quan trọng: Không phải “media query mobile-first”. Họ muốn viewport khác container query, type fluid tôn trọng zoom của user, CLS từ ảnh, 100vh khác dvh, và spacing có đến từ token không. Card 400px trong cửa sổ 1440px vẫn là mobile. @media trên card đó là sai chỗ phụ trách.
- Cách hoạt động:
@media (min-width: …)cho nav → sidebar (chrome trang).@containercho widget nằm sidebar hoặc cột main. Khai báocontainer-type: inline-sizetrên parent. Type fluid:clamp(1rem, 0.9rem + 0.4cqi, 1.25rem). Vẽ số hạng giữa để nó không thấp quá ở 320px. - Ảnh, ads, và embed nhận
aspect-ratio(hoặc attr width/height) trước load. Đó là CLS, không phải “lazy”.100dvhtheo toolbar động;svh/lvhkhi hiện/ẩn chrome quan trọng. Token sống trên:root/[data-theme], không phải pixel ma trong SFC Vue. - JS
innerWidthtrongsetup()là bug hydration: server và laptop không khớp. Nav off-canvas là chuyện viewport (@media), không phải container query. Analog Vue:ClientOnlykhông sửa CLS; hộp đã chừa mới sửa.
:root { --space: 1rem; --title: clamp(1.25rem, 1rem + 1cqi, 2rem); }
.page { container-type: inline-size; } /* widget query cái này, không phải viewport */
@media (min-width: 64rem) { .nav { display: block; } } /* chrome trang */
@container (min-width: 24rem) { .card { grid-template-columns: 8rem 1fr; } }
img { aspect-ratio: 16 / 9; width: 100%; height: auto; } /* chừa trước load, CLS */
.hero { min-height: 100dvh; } /* không 100vh: toolbar iOS */
/* đừng: @media trên .card chỉ vì cửa sổ 1440px */
/* đừng: clamp(20px, 4vw, 40px): min/max px phá zoom 200% */Điều gì xảy ra: Nav là quyết định của trang. Layout card theo container, nên card sidebar 400px thành một cột trên cửa sổ rộng. Ảnh giữ chiều cao trước khi byte tới. Type scale bằng rem. Hero theo viewport nhìn thấy trên iOS.
| Cơ chế | Phụ trách | Bẫy |
|---|---|---|
@media (min-width) | Chrome trang | Dùng nó trên widget 400px |
@container | Widget ở sidebar hoặc main | Nav off-canvas vẫn cần viewport |
clamp bằng rem | Type fluid + zoom | min/max px lúc zoom 200% |
aspect-ratio / dvh | CLS / chiều cao iOS | 100vh dưới toolbar |
Góc nhìn senior
- Card 400px trong cửa sổ 1440px vẫn là mobile. Container query ra đời đúng cho câu đó.
- min/max của
clampdùng rem để zoom 200% vẫn đạt reflow WCAG. Vẽ đồ thị phần giữacqi. - Nguồn CLS ngoài ảnh: webfont swap, banner chèn vào, skeleton
v-ifcủa Vue không chừa chiều cao, ads. 100vhkhác100dvhkhác100svh: viewport động, viewport nhỏ, viewport lớn. Full-screen trên mobile vẫn cần inset safe-area.- JS
innerWidthtrongsetup()lệch SSR. CSS là owner; effect matchMedia là fallback.
Lỗi hay gặp
- Triệu chứng: card sidebar trông như hàng desktop. Nguyên nhân:
@mediatrên card. Cách xử lý:@container.- Triệu chứng: nhảy 40px lúc ảnh load. Nguyên nhân: không có
aspect-ratio. Cách xử lý: chừa sẵn hộp.- Triệu chứng: chữ quá nhỏ lúc zoom 200%. Nguyên nhân:
clampbằng px. Cách xử lý: rem.- Triệu chứng: lệch hydration trên nav. Nguyên nhân:
innerWidthtrongsetup(). Cách xử lý: CSS, rồi effect.
Nhà tuyển dụng hay hỏi gì
Không phải trivia mobile-first. Họ muốn viewport khác container query, fluid type tôn trọng zoom, CLS từ ảnh, 100vh khác dvh, và token khác magic pixel.
Trả lời khi phỏng vấn
Mình media-query trang và container-query widget. Card 400px trong cửa sổ 1440px vẫn là mobile, nên media trên card đó là owner sai. Fluid type là clamp bằng rem để zoom 200 phần trăm vẫn chạy; mình vẽ đồ thị phần giữa cqi để không tụt dưới ở 320px. Ảnh nhận aspect-ratio trước khi load. Đó là CLS, không phải lazy. Chiều cao viewport là 100dvh, kèm inset safe-area. Token nằm trên root, không phải magic pixel trong SFC. innerWidth trong setup là bug hydration. Nav off-canvas vẫn là query viewport.
Nhớ nhanh
- Media-query trang. Container-query widget.
clampbằng rem. Vẽ đồ thị phần giữa. Zoom 200% vẫn phải chạy.- Chừa
aspect-ratiotrước khi load. Đó là CLS. 100dvhkèm safe-area. Không100vh. KhônginnerWidthtrongsetup().
Câu hỏi nối tiếp thường gặp
- Khi nào container query sai? Nav off-canvas: bạn cần viewport.
- Vì sao rem trong min/max clamp? Zoom 200%. Min px vẫn nhỏ về vật lý.
- Ba nguồn CLS ngoài ảnh? Webfont swap, banner chèn vào, skeleton v-if không chiều cao.
- 100vh khác dvh khác svh? Viewport động khác nhỏ khác lớn. Full-screen vẫn cần safe-area.
Tóm tắt: z-index chỉ so sánh phần tử trong cùng stacking context. Opacity, transform, filter, position + z-index, isolation, và will-change tạo context mới. “z-index: 9999 không chạy” nghĩa là modal và sticky header ở những context khác nhau. Vue transform trên parent là thủ phạm hay gặp.
Vì sao quan trọng: Modal nằm dưới sticky header, hoặc dropdown bị cắt dưới card có transform lúc hover. Senior nêu rõ context, không phải số lớn hơn. Teleport tới body là fix Vue khi parent đã tạo context.
- Cách hoạt động: Root là một stacking context. Child có
position(khôngstatic) vàz-indexkhácautothì tạo cái mới.opacity < 1,transform/filter/perspectivekhácnone,isolation: isolate,will-changecác property đó, vàfixedtrong một số trường hợp cũng vậy. - Trong một context, thứ tự vẽ là: background/border, rồi z-index âm, rồi in-flow, rồi float, rồi positioned
z-index: auto, rồi z-index dương.9999trong parentz-index: 1không thắng siblingz-index: 2của parent đó. - Vue:
transitiongántransform, hoặc cardtranslatelúc hover, nhốt descendant. Cách xử lý: Teleport overlay tớibody, hoặcisolationtrên root overlay riêng với thang rõ ràng. DevTools: view Layers 3D, hoặc outline xem ai tạo context.
/* Sticky header: position + z-index => stacking context */
.header { position: sticky; top: 0; z-index: 20; }
/* Transform hover của card => context MỚI. Dropdown bên trong không thắng .header */
.card:hover { transform: translateY(-2px); }
.card__menu { position: absolute; z-index: 9999; } /* bị nhốt */
/* Cách xử lý: root overlay trên body, một thang đã biết, không 9999 trong card */
.overlay-root { isolation: isolate; z-index: 40; }
/* Vue: <Teleport to="body"><div class="overlay-root">… */Điều gì xảy ra: Header là z-index: 20 ở context trang. transform của card tạo context local; 9999 chỉ thắng trong card đó. Teleport tới root overlay 40 so sánh lại với header.
| Tạo context | Triệu chứng | Cách xử lý |
|---|---|---|
transform / filter / opacity | Dropdown nằm dưới sibling | Teleport / bỏ transform |
position + z-index | 9999 thua 2 | So sánh cùng parent |
isolation: isolate | Thang overlay chủ đích | Một root overlay |
Vue <Transition> | Modal lúc leave nằm dưới header | Teleport trước, rồi transition |
Góc nhìn senior
- Nêu rõ context trước, rồi mới nói số.
z-indexlớn hơn là cách junior hay chốt. - Giữ một thang overlay (dropdown 30, modal 40, toast 50). 9999 đặt bừa là cách thua bug lần sau.
will-change: transformtạo context cả trước animation. Đừng gắn lung tung.- Để
fixedtrong parent đã transform thìfixedhành xử nhưabsolute. Cùng một họ bug. - Đo: Layers 3D trong DevTools, hoặc test case rút gọn với transform đã xóa.
Lỗi hay gặp
- Triệu chứng: modal dưới sticky header. Nguyên nhân: parent
transform/z-index: 1. Cách xử lý: Teleport tớibody.- Triệu chứng:
9999vẫn thua. Nguyên nhân: khác context. Cách xử lý: lên tới parent chung.- Triệu chứng: banner cookie
fixedvẫn cuộn. Nguyên nhân: ancestor đã transform. Cách xử lý: đưa nó ra.- Triệu chứng: dropdown sau cần
10000. Nguyên nhân: không có thang overlay. Cách xử lý: một root + token.
Nhà tuyển dụng hay hỏi gì
Vì sao modal nằm dưới sticky header, và vì sao z-index: 9999 không chạy? Họ muốn stacking context, không phải số lớn hơn.
Trả lời khi phỏng vấn
z-index chỉ so sánh phần tử trong cùng stacking context. Opacity, transform, filter, position kèm z-index, isolation, và will-change tạo cái mới. 9999 trong card đã transform không thắng sticky header z-index 20 trên trang. Mình nêu rõ context, rồi Teleport overlay tới body hoặc isolate một root overlay với thang rõ ràng: dropdown, modal, toast. Transform của Vue Transition là thủ phạm hay gặp. Mình không ship số lớn hơn. Layers trong DevTools là cách mình chứng minh.
Nhớ nhanh
z-indexchỉ so sánh trong một stacking context.transform/ opacity /position+z-indextạo context mới.9999thua nghĩa là khác context. Teleport hoặc một root overlay.- Giữ một thang overlay rõ ràng. Đừng gắn
will-changelung tung.
Câu hỏi nối tiếp thường gặp
- Liệt kê cái tạo stacking context? transform, filter, opacity nhỏ hơn 1, position kèm z-index, isolation, will-change.
- fixed trong parent đã transform? Hành xử như absolute. Đưa nó ra.
- Bẫy Transition Vue? Transform trên parent. Teleport trước.
- Chứng minh thế nào? Layers 3D DevTools, hoặc xóa transform trong case rút gọn.
Tóm tắt: Kích thước intrinsic là min-content, max-content, và fit-content. Overflow Grid/Flex thường là child không co dưới min-content. Overflow chữ cần min-width: 0 kèm chiến lược wrap/clip (truncate, anywhere). min-width: 0 là cùng một câu với 3.1, cố ý vậy.
Vì sao quan trọng: Ô table, tên file, hoặc flex child “không chịu co”. Senior nêu rõ min-content, không phải “thêm overflow hidden khắp nơi”. Overflow hidden trên body cũng fail reflow WCAG lúc zoom 400%.
- Cách hoạt động:
min-contentlà size nhỏ nhất hộp có thể có mà không tràn nội dung (từ dài nhất / không ngắt được).max-contentlà size nếu mọi thứ nằm một dòng.fit-contentlàmin(max-content, available)bị kẹp bởi min-content. - Flex/grid child mặc định
min-width: auto= min-content đó. Truncate:min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowraptrên text node (hoặc wrapper). URL và SKU:overflow-wrap: anywherenếu phải hiện cả chuỗi. overflow: autotrên cột vẫn cầnmin-height: 0/min-width: 0.overflow-x: hiddentrênbodyđể “giết scroll 1px” là cách zoom 400% fail Reflow 1.- 4.10. Bên Vue: key dài nội suy trong ô
v-for.
- 4.10. Bên Vue: key dài nội suy trong ô
.cell {
min-width: 0; /* dưới min-content: không có cái này thì truncate không bao giờ chạy */
}
.cell__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sku { overflow-wrap: anywhere; } /* hiện cả chuỗi không ngắt được */
.panel {
min-height: 0;
overflow: auto; /* cuộn bên trong, không phải trang */
}
/* đừng: overflow-x: hidden trên body để giấu scroll 1px. Reflow WCAG */Điều gì xảy ra: Ô được phép co dưới min-content của tên file, nên ellipsis chạy được. SKU wrap thay vì kéo giãn grid. Panel cuộn bên trong. body để nguyên cho zoom 400%.
| Keyword | Nghĩa | Dùng |
|---|---|---|
min-content | Nhỏ nhất mà không tràn | Min mặc định của flex/grid |
max-content | Size một dòng | Tooltip, chip |
fit-content | min(max-content, available) | Dialog ôm nội dung |
min-width: 0 | Cho phép co dưới min-content | Truncate / cuộn trong |
Góc nhìn senior
- Truncate mà thiếu
min-width: 0là no-op. Đó là demo phỏng vấn. - Chọn chiến lược: ellipsis (tên), wrap anywhere (SKU), cuộn trong (panel). Hidden-on-body không phải chiến lược.
- Đơn vị
ch/extheo glyph; không thay được min-content cho flex child. - Cùng câu chuyện với 3.1 và 3.2. Topic này nêu rõ các keyword intrinsic.
- Zoom 400% +
overflow-x: hiddentrênbodyfail WCAG 1.4.10. Sửa thủ phạm 1px thì hơn.
Lỗi hay gặp
- Triệu chứng: ellipsis không hiện. Nguyên nhân: parent vẫn
min-width: auto. Cách xử lý:min-width: 0trên flex/grid child.- Triệu chứng: grid bị URL kéo giãn. Nguyên nhân: min-content không gãy được. Cách xử lý:
overflow-wrap: anywherehoặc truncate.- Triệu chứng: zoom 400% cần cuộn hai trục. Nguyên nhân:
overflow-x: hiddentrênbody. Cách xử lý: xóa đi.- Triệu chứng: panel trong không cuộn. Nguyên nhân: thiếu
min-height: 0. Cách xử lý: Grid-of-Flex 3.3.
Nhà tuyển dụng hay hỏi gì
Vì sao flex child không chịu co, và truncate tên file thế nào mà không overflow: hidden trên body? Họ muốn bạn nêu rõ min-content.
Trả lời khi phỏng vấn
Kích thước intrinsic là min-content, max-content, và fit-content. Flex hay grid child mặc định min-width auto, tức min-content: từ dài nhất. Vì thế flex 1 không chặn được tên file. Mình để min-width 0 trên child, rồi chọn chiến lược: ellipsis cho tên, overflow-wrap anywhere cho SKU, overflow auto bên trong cho panel. Mình không giấu overflow trên body; cách này fail reflow khi zoom 400 phần trăm. fit-content là dialog ôm sát nội dung. Cùng câu với 3.1, nêu đúng tên.
Nhớ nhanh
min-content/max-content/fit-contentlà keyword intrinsic.min-width: 0cho child co dưới min-content để truncate chạy được.- Chọn ellipsis, wrap-anywhere, hoặc cuộn trong. Không dùng
overflow-x: hiddentrênbody. - Cùng câu chuyện co với 3.1 / 3.2, nêu là kích thước intrinsic.
Câu hỏi nối tiếp thường gặp
- min-content khác max-content khác fit-content? Chặt nhất / một dòng / min của max-content và available.
- Vì sao truncate không min-width 0 fail? Box không co xuống dưới từ.
- overflow-x hidden trên body? Fail WCAG 1.4.10 lúc zoom 400%. Sửa thủ phạm 1px.
- Đơn vị ch thay? Chúng theo glyph. Không ghi đè min-width auto.
Tóm tắt: Ba công cụ 2026: subgrid (track con theo parent), :has() (selector parent), @layer (thứ tự cascade bạn đặt tên). Cả ba là Baseline trên trình duyệt evergreen hiện đại. Vẫn kiểm tra support trên matrix của bạn, và có fallback một dòng. Chúng không thay 3.1–3.6.
Vì sao quan trọng: “Bạn có dùng :has trên production không?” muốn một câu support kèm một case (form invalid, card đang mở menu). @layer là cách design system bỏ !important. subgrid là cách card lồng share cột của trang mà không magic number.
- Cách hoạt động:
grid-template-columns: subgridtrên child (span cột parent) dùng lại các track đó: label trong form lồng canh nhau.:has()chọn parent khi descendant khớp (.card:has(dialog[open]),.field:has(:invalid)).@layer reset, tokens, base, components, utilities;rồi layer cuối thắng khi specificity bằng nhau. - Support (2026): Chromium, Firefox, Safari hiện tại đều ship những cái này.
@supportscho subgrid nếu bạn còn WebView long-tail.:has()không thayclassListJS cho state Vue đã có. Ưu tiên class nếu Vue biếtopen. @layerkhông thắng inline style hay animation. Utility ở layer cuối thay component mà không!important. Analog Vue: CSS scoped kèm một layer trong thứ tự import design-system. Fallback: Grid lồng với cùng sốminmax. Xấu, nhưng chạy.
@layer reset, tokens, base, components, utilities;
@layer components {
.form { display: grid; grid-template-columns: 8rem minmax(0, 1fr); }
.form > .row { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; }
.field:has(:invalid) { outline: 2px solid var(--danger); }
.card:has(dialog[open]) { z-index: 2; } /* stacking: vẫn nêu rõ context */
}
@layer utilities { .mt-0 { margin-top: 0; } } /* thắng components khi spec bằng */
@supports not (grid-template-columns: subgrid) {
.form > .row { grid-template-columns: 8rem minmax(0, 1fr); } /* track nhân đôi */
}Điều gì xảy ra: Các hàng lồng share hai cột của form qua subgrid. Input invalid style field mà Vue không gắn class. Utility nằm layer sau nên mt-0 thắng, không cần !important. WebView cũ nhân đôi track.
| Công cụ | Cho bạn | Đừng |
|---|---|---|
subgrid | Canh lồng trên track parent | Giả định mọi WebView; dùng @supports |
:has() | State parent từ descendant | Thay state Vue bạn đã có |
@layer | Cascade đặt tên; DS không !important | Mong nó thắng inline style |
Góc nhìn senior
- Kiểm tra support trên matrix của bạn. Evergreen 2026 thì ổn; WebView long-tail là chuyện
@supports. :has()có thể đắt trên cây lớn. Ưu tiên class Vue cho state app đã biết.- Thứ tự
@layerchính là product. Import DS trước, utility cuối. subgridvẫn cầnminmax(0, 1fr)trên parent. Bẫy intrinsic không biến mất.- Phần extra này không thay nền Flex/Grid. 3.1–3.6 vẫn giúp bạn được nhận.
Lỗi hay gặp
- Triệu chứng: label lồng lệch. Nguyên nhân: Grid lồng copy số. Cách xử lý:
subgrid+@supports.- Triệu chứng:
:has(:hover)giật trên list 10k. Nguyên nhân: selector đắt. Cách xử lý: class Vue trên hàng.- Triệu chứng: utility thua component. Nguyên nhân: sai thứ tự layer. Cách xử lý: utility cuối.
- Triệu chứng:
!importantvẫn khắp nơi. Nguyên nhân: chưa dùng@layer. Cách xử lý: thứ tự import, rồi cấm important mới.
Nhà tuyển dụng hay hỏi gì
Bạn có dùng :has, subgrid, và @layer trên production không? Họ muốn một câu support, một case thật mỗi cái, và fallback. Không phải đọc blog.
Trả lời khi phỏng vấn
Năm 2026 trên trình duyệt evergreen, ba cái này là Baseline, và mình vẫn kiểm tra matrix WebView. subgrid cho hàng lồng share track parent để label form canh nhau. Mình giữ fallback at-supports nhân đôi số minmax. has chọn parent từ descendant: field invalid, card đang mở dialog. Nếu Vue đã biết state, mình gán class thay; has có thể đắt trên list lớn. layer đặt tên cascade để design system bỏ important. Utility để cuối, nó không thắng inline style. Chúng không thay min-width 0.
Nhớ nhanh
subgriddùng lại track parent. Giữ fallback@supports.:has()là selector parent. Ưu tiên class Vue cho state đã biết.@layerđặt tên cascade. Utility cuối. Không mạnh hơn inline.- Kiểm tra support 2026 trên matrix của bạn. Phần extra này không thay 3.1–3.6.
Câu hỏi nối tiếp thường gặp
- subgrid khác Grid lồng copy minmax? subgrid vẫn canh khi parent đổi.
- has khác class Vue? Class nếu app đã biết open/invalid. has cho descendant thuần CSS.
- layer thắng inline? Không. Cũng không thắng animation. Nó thắng file viết sau khi cùng specificity.
- Câu support? Evergreen 2026 thì có; WebView long-tail dùng at-supports.