Hub

Layout CSS

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.

  1. Cách hoạt động: display: flex tạo flex formatting context. Trục main theo flex-direction. flex: 1 nghĩ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ặc overflow: hidden nếu bạn cũng clip) cho phép co.
  2. Chốt action bên phải bằng margin-inline-start: auto, không phải space-between trên hai item rồi v-if thêm child thứ ba. gap bỏ qua child display: none; margin :last-child thì không. flex-wrap trên CTA toolbar có thể đẩy nó xuống dưới fold: đó là bug product, không phải thắng CSS.
  3. CSS order là mùi a11y trong v-for Vue: thứ tự nhìn ≠ thứ tự tab ≠ thứ tự screen reader. Tương đương Vue: cùng câu chuyện min-width: 0 trê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 raNên ship
flex: 1 + từ dàiKhông co dưới min-width: automin-width: 0 (hoặc clip)
space-between + v-if thứ baKhoảng cách trông ngẫu nhiênmargin-inline-start: auto
Margin :last-child + filter VueGap cuốigap
flex-basis: auto card đềuPadding/border phá đềuflex: 1 1 0 / flex-basis: 0

Góc nhìn senior

  • Trục main là trục flex-direction chỉ tới. Cross là canh (align-items). Nói cả hai từ.
  • flex: 1 không phải giấy phép co. min-width: auto là tối thiểu nội dung. Đó là câu phỏng vấn.
  • gap khác margin quan trọng trong Vue vì v-if gỡ box mà không gỡ lỗ :last-child.
  • order và 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: auto vẫn cần min-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: wrap trê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 order trong v-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: 1 không thắng min-width: auto.
  • min-width: 0 trên child phải co.
  • gap chịu được v-if Vue. Margin :last-child thì không.
  • Chốt action bên phải bằng margin-inline-start: auto. Tránh CSS order.

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.

  1. Cách hoạt động: grid-template-columns và grid-template-rows tạo track. fr chia 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.
  2. repeat(auto-fit, minmax(min(100%, 16rem), 1fr)) lấp đầy gallery mà không cần breakpoint theo số card. auto-fit gộp track trống; auto-fill giữ chúng. min(100%, 16rem) cứu bạn ở 320px chỗ minmax(200px, 1fr) tràn.
  3. Ẩn inspector trong Vue bằng cách đổi area / column, không phải display: none trên một area để lại track trống. position: sticky fail nếu grid item (hoặc main) là ancestor overflow. Tương đương Vue: child v-for chư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.

PatternDùng khiBẫy
Area đặt tênShell appẨn cột Vue sai cách để lại track
1frTrack co giãnNghĩ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-fillGiữ 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.
  • 1fr là cùng bẫy min-content như Flex. minmax(0, 1fr) là giấy phép co.
  • auto-fit khác auto-fill: gộp khác giữ track trống. Thư viện widget thường muốn fill.
  • grid-auto-flow: dense lấp lỗ và phá thứ tự nhìn so với thứ tự tab. Cùng mùi a11y với order.
  • Sticky trong main fail khi main overflow. Nêu rõ ancestor overflow.

Lỗi hay gặp

  • Triệu chứng: cột 1fr trà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ại grid-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.
  • 1fr là minmax(auto, 1fr). Dùng minmax(0, 1fr) để co.
  • auto-fit gộp track trống; auto-fill giữ chúng.
  • min(100%, 16rem) trong minmax cứ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.

  1. 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.vue làm Grid, còn trang chỉ Flex nội dung. Flex cho component: nav, cụm button, input+addon, icon+copy empty-state.
  2. 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à 1fr main trong Grid; align-items: center trên trang Flex ngắn phá “footer ở đáy”.
  3. 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ốngFlexGrid
Chrome trangỔn với một cột; mong manh khi có haiArea đặt tên: phụ trách trang
ToolbarPhân phối một trục tự nhiênTên line thừa
Gallery cardWrap lệch hàngTrack canh cả hai trục
Sticky footerCenter trên trang ngắn thì hỏng1fr 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: 0 thuộ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.vue Grid, 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: 0 trê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: 0 trê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.

  1. Cách hoạt động: @media (min-width: …) cho nav → sidebar (chrome trang). @container cho widget nằm sidebar hoặc cột main. Khai báo container-type: inline-size trê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.
  2. Ảnh, ads, và embed nhận aspect-ratio (hoặc attr width/height) trước load. Đó là CLS, không phải “lazy”. 100dvh theo toolbar động; svh / lvh khi hiện/ẩn chrome quan trọng. Token sống trên :root / [data-theme], không phải pixel ma trong SFC Vue.
  3. JS innerWidth trong setup() 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: ClientOnly khô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áchBẫy
@media (min-width)Chrome trangDùng nó trên widget 400px
@containerWidget ở sidebar hoặc mainNav off-canvas vẫn cần viewport
clamp bằng remType fluid + zoommin/max px lúc zoom 200%
aspect-ratio / dvhCLS / chiều cao iOS100vh 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 clamp dùng rem để zoom 200% vẫn đạt reflow WCAG. Vẽ đồ thị phần giữa cqi.
  • Nguồn CLS ngoài ảnh: webfont swap, banner chèn vào, skeleton v-if của Vue không chừa chiều cao, ads.
  • 100vh khác 100dvh khác 100svh: viewport động, viewport nhỏ, viewport lớn. Full-screen trên mobile vẫn cần inset safe-area.
  • JS innerWidth trong setup() 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: @media trê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: clamp bằng px. Cách xử lý: rem.
  • Triệu chứng: lệch hydration trên nav. Nguyên nhân: innerWidth trong setup(). 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.
  • clamp bằng rem. Vẽ đồ thị phần giữa. Zoom 200% vẫn phải chạy.
  • Chừa aspect-ratio trước khi load. Đó là CLS.
  • 100dvh kèm safe-area. Không 100vh. Không innerWidth trong setup().

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.

  1. Cách hoạt động: Root là một stacking context. Child có position (không static) và z-index khác auto thì tạo cái mới. opacity < 1, transform / filter / perspective khác none, isolation: isolate, will-change các property đó, và fixed trong một số trường hợp cũng vậy.
  2. 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. 9999 trong parent z-index: 1 không thắng sibling z-index: 2 của parent đó.
  3. Vue: transition gán transform, hoặc card translate lúc hover, nhốt descendant. Cách xử lý: Teleport overlay tới body, hoặc isolation trê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 contextTriệu chứngCách xử lý
transform / filter / opacityDropdown nằm dưới siblingTeleport / bỏ transform
position + z-index9999 thua 2So sánh cùng parent
isolation: isolateThang overlay chủ đíchMột root overlay
Vue <Transition>Modal lúc leave nằm dưới headerTeleport trước, rồi transition

Góc nhìn senior

  • Nêu rõ context trước, rồi mới nói số. z-index lớ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: transform tạo context cả trước animation. Đừng gắn lung tung.
  • Để fixed trong parent đã transform thì fixed hà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ới body.
  • Triệu chứng: 9999 vẫ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 fixed vẫ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-index chỉ so sánh trong một stacking context.
  • transform / opacity / position+z-index tạo context mới.
  • 9999 thua 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-change lung 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%.

  1. Cách hoạt động: min-content là 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-content là size nếu mọi thứ nằm một dòng. fit-content là min(max-content, available) bị kẹp bởi min-content.
  2. Flex/grid child mặc định min-width: auto = min-content đó. Truncate: min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap trên text node (hoặc wrapper). URL và SKU: overflow-wrap: anywhere nếu phải hiện cả chuỗi.
  3. overflow: auto trên cột vẫn cần min-height: 0 / min-width: 0. overflow-x: hidden trên body để “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.
.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%.

KeywordNghĩaDùng
min-contentNhỏ nhất mà không trànMin mặc định của flex/grid
max-contentSize một dòngTooltip, chip
fit-contentmin(max-content, available)Dialog ôm nội dung
min-width: 0Cho phép co dưới min-contentTruncate / cuộn trong

Góc nhìn senior

  • Truncate mà thiếu min-width: 0 là 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 / ex theo 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: hidden trên body fail 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: 0 trê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: anywhere hoặc truncate.
  • Triệu chứng: zoom 400% cần cuộn hai trục. Nguyên nhân: overflow-x: hidden trên body. 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-content là keyword intrinsic.
  • min-width: 0 cho 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: hidden trên body.
  • 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.

  1. Cách hoạt động: grid-template-columns: subgrid trê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.
  2. Support (2026): Chromium, Firefox, Safari hiện tại đều ship những cái này. @supports cho subgrid nếu bạn còn WebView long-tail. :has() không thay classList JS cho state Vue đã có. Ưu tiên class nếu Vue biết open.
  3. @layer khô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
subgridCanh lồng trên track parentGiả định mọi WebView; dùng @supports
:has()State parent từ descendantThay state Vue bạn đã có
@layerCascade đặt tên; DS không !importantMong 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ự @layer chính là product. Import DS trước, utility cuối.
  • subgrid vẫn cần minmax(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: !important vẫ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

  • subgrid dù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.