0

[Series 100 Bài Học HTML/CSS Thực Chiến - Bài 08]: Làm Chủ Sticky Header Bám Dính Khi Cuộn Trang & Thiết Lập Hệ Thống Phân Tầng z-index Chuẩn Kiến Trúc Lớn

Chào mừng anh em đã bước sang Bài 08 trong chuỗi 100 Bài Học Làm Chủ HTML/CSS Qua Dự Án Clone Shopee!

Trải qua 7 bài học đầu tiên, chiếc Header màu cam rực rỡ của chúng ta đã có đầy đủ mọi "vũ khí": Top Navbar, Popover QR Code, Hộp Thông báo, Khung tìm kiếm kèm Lịch sử và Giỏ hàng Mini.

Tuy nhiên, khi trang chủ Shopee có hàng ngàn sản phẩm bên dưới và người dùng bắt đầu lăn con trỏ chuột xuống sâu, một vấn đề trải nghiệm người dùng (UX) lập tức phát sinh: Toàn bộ thanh Header bị trôi tuột lên trên và biến mất khỏi màn hình! Lúc này, nếu khách hàng chợt nhớ ra món đồ cần tìm hoặc muốn kiểm tra Giỏ hàng, họ lại phải cuộn chuột ngược lên tận cùng rất mất thời gian.

Trong Bài 08 hôm nay — bài học khép lại Chương 1 (Chinh phục Header Shopee), chúng ta sẽ giải quyết bài toán này bằng kỹ thuật Sticky / Fixed Header, đồng thời xóa bỏ thói quen đặt z-index: 999999 vô tội vạ bằng cách xây dựng Hệ thống phân tầng z-index (Stacking Context) chuẩn mực của các dự án E-Commerce lớn!

1. Cuộc chiến định vị khi cuộn trang: Chọn position: fixed hay position: sticky?

Trong CSS hiện đại, để ghim chặt thanh <header class="shopee-header"> luôn bám dính ở mép trên cùng màn hình (top: 0), chúng ta có 2 sự lựa chọn là position: fixed và position: sticky. Rất nhiều lập trình viên nhầm lẫn hai thuộc tính này là một, nhưng bản chất hoạt động của chúng trên luồng tài liệu (Document Flow) lại khác nhau hoàn toàn.

Hãy quan sát sơ đồ so sánh cơ chế hoạt động giữa position: sticky và position: fixed dưới đây:

image.png

Để chọn được phương án tối ưu nhất cho Header Shopee, hãy đặt hai thuộc tính này lên bàn cân:

Tiêu chí so sánh position: fixed (Cố định tuyệt đối) position: sticky (Bám dính thông minh)
Vị trí trong luồng HTML (Document Flow) Bị nhấc bổng hoàn toàn khỏi mặt đất. Các phần tử bên dưới (<main>) sẽ lập tức tràn lên chiếm chỗ của Header. Vẫn giữ nguyên chỗ đứng trong luồng trang giống relative, chỉ biến thành fixed khi cuộn chạm mốc top: 0.
Cách xử lý khoảng trống Bắt buộc phải bù thêm margin-top: 120px (hoặc padding-top) cho thẻ <main> để nội dung đầu trang không bị Header che khuất. Không cần bù margin-top! Thẻ <main> tự động nằm ngay dưới đáy Header một cách tự nhiên.
Phạm vi hoạt động Bám theo cửa sổ trình duyệt (Viewport) từ đầu đến cuối trang. Bám trong phạm vi thẻ cha trực tiếp chứa nó (Ở đây thẻ cha là <body> nên nó cũng bám suốt chiều dài trang).
Chốt phương án cho Shopee Dùng khi cần ghim cố định mọi lúc, mọi nơi bất kể cấu trúc thẻ cha. Khuyên dùng cho Header chính vì code gọn, không lo lệch chiều cao khi Header thay đổi kích thước trên Mobile!

Cảnh báo "Bẫy vô hiệu hóa position: sticky": Thuộc tính position: sticky sẽ tự động chết (không hoạt động) nếu bất kỳ thẻ cha nào bọc ngoài nó (như body hoặc html) khai báo thuộc tính overflow: hidden, overflow: auto hoặc overflow: scroll! Do đó, hãy luôn giữ body thông thoáng.

2. Giải quyết "Cơn ác mộng z-index: 999999" bằng Hệ thống Phân Tầng (Z-Index Scale)

Khi bạn ghim thanh Header bằng position: sticky và cuộn chuột xuống dưới, bạn sẽ gặp một hiện tượng rất quái dị: Các khối Banner quảng cáo, nhãn giảm giá "Giảm 50%" hoặc đồng hồ Flash Sale ở phần thân trang (vốn có position: relative hoặc absolute) sẽ trồi lên đè xuyên thủng qua cả nền cam của Header!

Lý do là trong không gian 3 chiều của trình duyệt (Trục X ngang, Trục Y dọc, và Trục Z chiều sâu hướng vào mắt người nhìn), phần tử nào viết sau trong file HTML (<main> viết sau <header>) sẽ mặc định nằm đè lên phần tử viết trước nếu chúng cùng cấp độ định vị.

Hãy nhìn vào mô hình phân lớp không gian 3 chiều theo trục Z (Stacking Context) của trình duyệt dưới đây:

image.png

Khi gặp lỗi bị đè giao diện, phản xạ của người mới là gõ bừa z-index: 999. Hôm sau làm cái Popup lại gõ z-index: 9999. Tuần sau làm cái khung Chat lại gõ z-index: 9999999! Cuối cùng không ai biết lớp nào đang nằm trên lớp nào.

Giải pháp chuẩn kiến trúc E-Commerce: Chúng ta quy hoạch toàn bộ chiều sâu của trang Shopee thành 5 tầng độ cao chuẩn và khai báo thành biến trong :root (base.css):

  1. Tầng 1 (--z-base: 1 →\rightarrow 9): Dành cho các nhãn nhỏ nằm đè lên ảnh sản phẩm (Nhãn Yêu thích, nhãn Giảm giá 50%, nút mũi tên qua lại của Banner).

  2. Tầng 2 (--z-sticky: 100): Dành riêng cho thanh Sticky Header (<header class="shopee-header">). Vì 100 lớn hơn 9, Header khi cuộn xuống sẽ đè bẹp toàn bộ nhãn sản phẩm và banner bên dưới!

  3. Tầng 3 (--z-dropdown: 200): Dành cho các hộp thoại xổ xuống bên trong Header (QR Code, Thông báo, Lịch sử tìm kiếm, Giỏ hàng mini).

  4. Tầng 4 (--z-fixed-widget: 300): Dành cho nút Cuộn lên đầu trang hoặc khung Chat hỗ trợ nằm góc dưới bên phải màn hình.

  5. Tầng 5 (--z-modal: 1000): Tầng tối cao dành cho màn đen phủ toàn trang khi bật Popup Đăng Ký / Đăng Nhập hoặc Popup Quảng cáo giữa màn hình.

3. Thực hành: Nâng cấp Sticky Header & Quy hoạch z-index

Bước 1: Khai báo bảng phân tầng z-index trong ./assets/css/base.css

Mở file base.css, tìm đến khối :root và bổ sung bộ biến quản lý tầng chiều sâu ngay bên dưới phần kích thước khung:

CSS

/* Bổ sung vào trong :root của file ./assets/css/base.css */
:root {
    /* ... (Giữ nguyên các biến màu sắc và kích thước ở Bài 02) ... */

    /* HỆ THỐNG PHÂN TẦNG CHIỀU SÂU (Z-INDEX SCALE) */
    --z-negative: -1;
    --z-elevate: 1;         /* Nhãn Yêu thích, Badge giảm giá trên thẻ sản phẩm */
    --z-sticky: 100;        /* Thanh Header bám dính khi cuộn trang */
    --z-dropdown: 200;      /* Hộp QR, Thông báo, Lịch sử tìm kiếm, Giỏ hàng */
    --z-fixed-widget: 300;  /* Khung Chat mini góc dưới màn hình */
    --z-modal: 1000;        /* Popup Đăng nhập / Quảng cáo phủ toàn màn hình */
}

Bước 2: Kích hoạt position: sticky cho .shopee-header trong ./assets/css/main.css

Hãy mở file main.css, tìm đến class .shopee-header ở ngay đầu file và nâng cấp nó với 3 dòng thuộc tính "quyền lực":

CSS

/* Cập nhật lại .shopee-header trong ./assets/css/main.css */

.shopee-header {
    height: var(--header-height);
    background-image: var(--shopee-primary-gradient);
    color: var(--white-color);
    
    /* KÍCH HOẠT STICKY HEADER BÁM DÍNH ĐỈNH MÀN HÌNH */
    position: sticky;
    top: 0;
    left: 0;
    right: 0;
    z-index: var(--z-sticky);                   /* Tầng 100: Luôn nổi trên toàn bộ nội dung thân trang */
    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.12); /* Tạo bóng đổ nhẹ dưới đáy Header khi cuộn qua nền trắng */
}

Đồng thời, hãy tìm và thay thế các con số z-index: 10 hoặc z-index: 9 mà chúng ta đã gõ tạm ở các bài trước bằng biến --z-dropdown chuẩn chỉnh:

CSS

/* Trong ./assets/css/main.css, cập nhật lại z-index cho 4 hộp Popover: */

.header-qr {
    /* ... các thuộc tính cũ giữ nguyên ... */
    z-index: var(--z-dropdown);
}

.header-notify {
    /* ... các thuộc tính cũ giữ nguyên ... */
    z-index: var(--z-dropdown);
}

.header-search-history {
    /* ... các thuộc tính cũ giữ nguyên ... */
    z-index: var(--z-dropdown);
}

.header-cart-list {
    /* ... các thuộc tính cũ giữ nguyên ... */
    z-index: var(--z-dropdown);
}

Bước 3: Tạo độ cao giả lập cho phần Thân trang để kiểm nghiệm hiệu ứng cuộn

Hiện tại các khối <section> trong <main> của chúng ta còn khá ngắn nên trang web chưa xuất hiện thanh cuộn dọc. Hãy tạm thời thêm chiều cao tối thiểu (min-height) cho các khối nội dung trong main.css để bạn có thể lăn chuột kiểm tra Sticky Header mượt mà ngay lập tức:

CSS

/* Cập nhật tạm trong ./assets/css/main.css để tạo thanh cuộn trang */

.hero-banner-section,
.category-section,
.flash-sale-section,
.daily-discover-section {
    background-color: var(--white-color);
    margin-top: 1.5rem;
    padding: 2rem;
    border-radius: 2px;
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
    min-height: 35rem;              /* Mỗi khối cao tối thiểu 350px để trang dài ra > 1500px */
    position: relative;             /* Giả lập các khối bên dưới đều có định vị relative */
    z-index: var(--z-elevate);      /* Tầng 1: Thấp hơn tầng 100 của Header */
}

image.png

4. Giải mã dòng code: Bẫy "Stacking Context" mà 80% lập trình viên mắc phải

Khi làm việc với z-index, có một quy tắc ngầm cực kỳ quan trọng mà bạn phải khắc cốt ghi tâm: z-index của thẻ con không thể vượt qua mặt thẻ cha của nó nếu thẻ cha đã tạo ra một Stacking Context riêng!

Hãy xem xét tình huống thực tế trên trang Shopee của chúng ta:

  • Thanh <header class="shopee-header"> có z-index: 100.

  • Bên trong Header có hộp <div class="header-notify"> mang z-index: 200.

  • Bên dưới thân trang có khối <section class="flash-sale-section"> mang z-index: 1.

Câu hỏi phỏng vấn kinh điển:

"Giả sử một lập trình viên lỡ tay đặt cho khối .flash-sale-section ở thân trang z-index: 150 (lớn hơn 100 của Header, nhưng nhỏ hơn 200 của hộp Thông báo). Vậy khi cuộn trang xuống và mở hộp Thông báo ra, hộp Thông báo (200) nằm TRÊN hay nằm DƯỚI khối Flash Sale (150)?"

Đáp án: Hộp Thông báo (200) sẽ bị khối Flash Sale (150) đè bẹp ở bên dưới!

Giải thích: Vì hộp Thông báo là con ruột của .shopee-header (z-index: 100), nên con số 200 của nó chỉ có giá trị so sánh với các anh em bên trong phạm vi của Header mà thôi! Khi bước ra ngoài so sánh với .flash-sale-section, trình duyệt sẽ lấy cấp độ của hai thẻ cha đem so với nhau: Header (100) thua Flash Sale (150) →\rightarrow Toàn bộ con cái của Header đều phải nằm dưới Flash Sale!

Nhờ việc chúng ta quy hoạch biến :root ngay từ đầu (--z-elevate: 1 cho thân trang và --z-sticky: 100 cho Header), bi kịch "con lớn mà cha nhỏ" này sẽ không bao giờ xảy ra trong suốt 100 bài học!

5. Bài tập thực nghiệm cuối Chương 1 & Bước sang Chương 2!

Hãy lưu lại tất cả các file, mở trình duyệt lên và thử lăn con trỏ chuột từ trên xuống dưới! Bạn sẽ thấy thanh Header Shopee bám chặt ở đỉnh màn hình mượt như lụa, các khối màu trắng của phần thân trang lướt nhẹ nhàng bên dưới lớp bóng đổ của Header. Dù bạn cuộn đến cuối trang, bạn vẫn có thể bấm vào ô tìm kiếm hoặc rê chuột mở Giỏ hàng, Thông báo bất cứ lúc nào!

Thử thách tổng kết Chương 1 dành cho bạn:

  1. Hãy thử vào main.css, sửa dòng z-index: var(--z-sticky); của .shopee-header thành z-index: 0; rồi cuộn chuột xuống dưới. Bạn sẽ thấy ngay thảm họa các khối trắng của phần thân trang trồi lên che mất cả chữ và giỏ hàng trên Header! (Thử xong nhớ trả lại var(--z-sticky) nhé).

  2. Thử đổi position: sticky của .shopee-header thành position: fixed. Bạn sẽ thấy khối "Khuyến mãi nổi bật" bên dưới lập tức bị tụt ngược lên trên và chui tọt vào gầm của Header (mất đi 120px chiều cao). Qua đó bạn sẽ thấy position: sticky tiện lợi hơn position: fixed như thế nào!

🎉 CHÚC MỪNG BẠN ĐÃ HOÀN THÀNH CHƯƠNG 1 (BÀI 01 – BÀI 08): LÀM CHỦ HEADER SHOPEE! Từ Bài 09, chúng ta sẽ chính thức bước sang Chương 2: Giải Phẫu Khu Vực Hero Banner & Làm Chủ CSS Grid Layout — nơi chúng ta sẽ dựng cụm Banner quảng cáo 2 tầng bất đối xứng kết hợp dãy 8 Icon dịch vụ (Shopee Mall, Săn Sale 1K, Mã Giảm Giá) cực kỳ bắt mắt!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí