Table of Contents6 sections
Bạn đã bao giờ thấy dòng này trong codebase chưa?
z-index: 10001 !important;Và ngay lập tức nghĩ: "Ủa, sao lại là 10001? Tại sao không phải 10000 hay 9999?" Nếu bạn đã từng đặt câu hỏi đó — hoặc tệ hơn là từng viết ra dòng đó — thì bài này dành cho bạn.
Cuộc chiến số không hồi kết
Trong hầu hết các dự án lớn có nhiều người cùng làm, z-index sẽ dần biến thành một bãi chiến trường. Team A đặt modal là 1000. Team B thấy toast bị che khuất, đặt lên 2000. Một tuần sau có người từ marketing nhét vào một cái cookie banner với z-index: 9999. Rồi ai đó thêm thư viện bên ngoài với z-index: 10000. Và thế là bạn phải đặt thứ của mình lên 10001.
Tất cả đều xuất phát từ một tâm lý rất đơn giản: "Nếu mình đặt số thật cao, chắc chắn nó sẽ nằm trên hết."
Đây là tư duy sai hoàn toàn — và hiểu tại sao sai sẽ giúp bạn viết CSS tốt hơn nhiều.
Stacking Context — thứ mà nhiều người bỏ qua
Trước khi nói đến giải pháp, cần hiểu tại sao z-index: 9999 đôi khi vẫn không ăn thua.
Như hình minh họa ở trên: z-index chỉ có ý nghĩa trong cùng một Stacking Context. Nếu hai phần tử nằm ở hai ngữ cảnh khác nhau, trình duyệt so sánh ngữ cảnh cha trước — rồi mới so sánh bên trong từng ngữ cảnh.
Ví dụ cụ thể: nếu phần tử cha của bạn có z-index: 1, còn phần tử cha của đối thủ có z-index: 2, thì dù con của bạn có đặt z-index: 99999, nó vẫn thua con của đối thủ chỉ với z-index: 1. Vì ngữ cảnh cha đã quyết định kết quả từ trước rồi.
Đây cũng là lý do tại sao khi bạn "debug mãi không ra tại sao nó bị che", câu trả lời thường không nằm ở giá trị z-index — mà nằm ở ngữ cảnh xếp chồng mà bạn đang hoạt động trong đó.
Mẹo nhỏ: Giá trị tối đa của
z-indexlà 2147483647 — đây là số nguyên có dấu lớn nhất của kiểu 32-bit. Đặt cao hơn số này, trình duyệt sẽ tự kéo về giới hạn đó.
Giải pháp: dùng biến CSS như một hệ thống layer có tên
Cách tốt nhất để thoát khỏi vòng lặp "số càng to càng tốt" là đặt tên cho từng tầng thay vì dùng magic number.
Như hình minh họa ở trên, thay vì cả team ai muốn đặt số gì thì đặt, bạn khai báo toàn bộ hệ thống tầng một chỗ duy nhất:
:root {
--z-base: 0;
--z-sidebar: 100;
--z-toast: 200;
--z-popup: 300;
--z-overlay: 400;
}Rồi dùng nó thay vì số cứng:
.modal { z-index: var(--z-popup); }
.toast { z-index: var(--z-toast); }
.cookie-banner { z-index: var(--z-overlay); }Ngay lập tức bạn có được ba lợi ích:
Dễ bảo trì: Toàn bộ thứ tự tầng nằm một chỗ. Muốn biết cái gì nằm trên cái gì — đọc
:rootlà xong, không cần lùng sục qua hàng chục file.Không còn va chạm: Mỗi loại phần tử có tầng riêng của nó. Không ai cần "đoán" xem team khác đang dùng số nào.
Thêm layer mới cực dễ: Cần thêm sidebar nằm giữa base và toast? Chỉ cần thêm một dòng và điều chỉnh lại các con số — không cần đụng đến một component nào:
:root {
--z-base: 0;
--z-sidebar: 100; /* mới thêm */
--z-toast: 200; /* đẩy lên */
--z-popup: 300;
--z-overlay: 400;
}Dùng calc() để buộc các thứ luôn đi cùng nhau
Có những trường hợp hai phần tử luôn phải đi kèm nhau — ví dụ overlay và cái nền mờ phía sau overlay đó. Thay vì tạo thêm một biến riêng cho nền mờ, hãy dùng calc():
.overlay-backdrop {
z-index: calc(var(--z-overlay) - 1);
}Giờ thì dù bạn đổi --z-overlay thành bất kỳ con số nào, nền mờ sẽ luôn luôn nằm ngay dưới overlay một bậc. Hai thứ này không bao giờ bị tách rời nhau dù code thay đổi thế nào.
Token toàn cục vs Token nội bộ — phân biệt để dùng đúng

Như hình minh họa ở trên, cần phân biệt hai loại token:
Token toàn cục dùng cho các tầng của toàn trang: modal, toast, sidebar, overlay. Đây là những thứ nổi lên trên mọi nội dung khác.
Token nội bộ dùng bên trong từng thành phần:
:root {
--z-top: 10;
--z-bottom: -10;
}Ví dụ: tooltip bên trong một modal chỉ cần nằm trên nội dung của modal đó — nó không cần biết modal đang ở tầng 300 hay 500. Vì bên trong một ngữ cảnh xếp chồng riêng, con số toàn cục không còn ý nghĩa nữa.
.modal-close-btn { z-index: var(--z-top); }
.toast-icon-decoration { z-index: var(--z-bottom); }Tooltip bên trong modal, icon trang trí bên trong toast — tất cả đều dùng token nội bộ, không cần quan tâm đến cuộc chiến số ngoài kia.
Năm quy tắc vàng khi làm việc với z-index
Không bao giờ dùng số tùy tiện:
999,10001,99999— đó đều là bug đang chờ bộc phát. Nếu một con số không gắn với hệ thống, nó không có chỗ trong codebase.Mọi
z-indexphải đến từ một biến: Dù là token toàn cục hay token nội bộ, luôn dùng biến thay vì số cứng.Khi bị che khuất — đừng tăng số trước: Vấn đề gần như chắc chắn nằm ở ngữ cảnh xếp chồng, không phải giá trị. Hãy kiểm tra cha của phần tử đó trước.
Nghĩ theo tầng, không theo số: Thay vì hỏi "cần đặt bao nhiêu?", hỏi "phần tử này thuộc tầng nào?"
Dùng
calc()để giữ các thứ liên quan luôn đi cùng nhau: Đừng để overlay và nền mờ của nó có hai token hoàn toàn độc lập.
z-index tự nó không phức tạp — cái phức tạp là khi cả team ai cũng tự ý đặt số theo cảm hứng. Một hệ thống token đơn giản có thể giải quyết phần lớn headache đó, và việc thiết lập nó chỉ tốn vài phút. Bắt đầu từ dự án tiếp theo của bạn thôi!
