[Series 100 Bài Học HTML/CSS Thực Chiến - Bài 05]: Xây Dựng Hộp Thoại Thông Báo (Notification Dropdown), Thủ Thuật Vẽ Mũi Tên Bằng CSS Border & Hiệu Ứng Nở Ra Từ Tâm (transform-origin)
Chào mừng anh em đã đến với Bài 05 trong chuỗi 100 Bài Học Làm Chủ HTML/CSS Qua Dự Án Clone Shopee!
Ở Bài 04, chúng ta đã hoàn thiện hộp thoại QR Code bên nhánh trái của Top Navbar. Hôm nay, chúng ta sẽ di chuyển sang nhánh phải để giải quyết một thành phần phức tạp và "xịn sò" hơn rất nhiều: Hộp thoại Thông Báo Mới Nhận (Notification Popover) khi người dùng rê chuột vào mục "Thông Báo".
Nếu bạn vào trang chủ Shopee và rê chuột vào quả chuông Thông báo, hãy quan sát thật kỹ 3 chi tiết đắt giá sau:
-
Hộp thoại không chỉ hiện ra đơn thuần (
fadeIn), mà nó phóng to dần ra từ đúng vị trí chữ "Thông Báo" ở góc trên bên phải! -
Phía trên nóc hộp thoại có một mũi tên hình tam giác nhọn màu trắng chỉ thẳng vào chữ "Thông Báo".
-
Danh sách bên trong phân biệt rõ Thông báo chưa đọc (nền cam phấn nhạt) và Thông báo đã đọc (nền trắng), đi kèm ảnh thumbnail sản phẩm vuông vức không bị méo hình.
Trong Bài 05 này, chúng ta sẽ giải mã toàn bộ 3 kỹ thuật đỉnh cao đó chỉ bằng HTML và CSS thuần!
1. Giải mã bí thuật: Vẽ hình tam giác nhọn bằng border trong CSS
Rất nhiều người mới thắc mắc: "Mọi thẻ HTML đều là hình chữ nhật, vậy làm sao các kỹ sư Shopee vẽ được cái mũi tên hình tam giác nhọn hoắt trồi lên trên nắp hộp thông báo?"
Câu trả lời nằm ở cách trình duyệt vẽ đường viền (border) tại 4 góc tiếp giáp nhau. Khi một chiếc hộp có chiều rộng width: 0 và chiều cao height: 0, nếu bạn đặt border thật dày (ví dụ 20px), 4 cạnh top, right, bottom, left sẽ chụm đầu vào nhau tạo thành 4 hình tam giác:
Plaintext
\ BORDER-TOP (Transparent) /
\ /
BORDER \ / BORDER
LEFT \ / RIGHT
(Trans) \ / (Trans)
\ BORDER-BOTTOM /
\ (Màu Trắng #fff)/
\_______________/
Nguyên lý cốt lõi:
Để tạo ra một mũi tên màu trắng chỉ thẳng lên trời:
Chúng ta cho phần tử giả
::beforecówidth: 0; height: 0;Đặt độ dày viền
border-width: 20px 26px;(Cao 20px, Rộng 26px để tam giác hơi bè ra giống Shopee).Tô màu trong suốt (
transparent) cho 3 cạnhTop,Left,Rightvà chỉ giữ lạiborder-bottom-color: #ffffff(Màu trắng)! Lập tức, một hình tam giác nhọn hướng lên trên sẽ xuất hiện!
2. Hiệu ứng nở ra từ góc (transform: scale + transform-origin)
Ở Bài 04, hộp QR Code chỉ hiện dần bằng opacity (0 1). Nhưng với hộp Thông Báo của Shopee, khi rê chuột vào, hộp thoại có cảm giác bung ra từ đúng tâm của quả chuông.
Để làm được điều này, chúng ta kết hợp 2 thuộc tính:
-
@keyframes headerNotifyGrowth: Kết hợp cảopacity(từ0lên1) vàtransform: scale()(phóng to kích thước từ0lên1). -
transform-origin: calc(100% - 3.2rem) top;: Mặc định,scale()sẽ phóng to từ tâm chính giữa hộp (50% 50%). Thuộc tínhtransform-origingiúp chúng ta dời "tâm phóng to" lên cạnh trên cùng (top) và cách mép phải32px(ngay đúng vị trí mũi tên tam giác chỉ vào chữ Thông Báo)!
| Thuộc tính | Giá trị mặc định | Giá trị tùy chỉnh chuẩn Shopee | Hiệu ứng thị giác mang lại |
|---|---|---|---|
transform: scale() |
scale(1) |
Đi từ scale(0) scale(1) |
Hộp thoại phóng to từ một điểm nhỏ xíu ra kích thước thật 404px. |
transform-origin |
center center (50% 50%) |
calc(100% - 3.2rem) top |
Hộp thoại bung xòe ra từ đúng đỉnh mũi tên góc trên bên phải. |
will-change |
auto |
opacity, transform |
Báo trước cho card đồ họa (GPU) của trình duyệt tối ưu hóa chuyển động 60fps không giật lag. |
3. Thực hành: Dựng HTML & CSS cho Hộp Thoại Thông Báo
Bước 1: Cập nhật HTML mục "Thông Báo" trong index.html
Hãy mở file index.html, tìm đến cụm <ul class="nav-list"> bên phải của .top-navbar. Chúng ta sẽ nâng cấp thẻ <li> của mục Thông Báo bằng cách thêm class .nav-item--has-notify và nhúng toàn bộ cấu trúc danh sách thông báo vào bên trong nó:
HTML
<!-- Tìm đến mục Thông Báo trong cụm <ul class="nav-list"> bên phải -->
<li class="nav-item nav-item--has-notify">
<a href="#" class="nav-link">
<i class="nav-icon fa-regular fa-bell"></i>
Thông Báo
</a>
<!-- HỘP THOẠI THÔNG BÁO MỚI NHẬN (Mặc định ẩn, hiện khi Hover) -->
<div class="header-notify">
<!-- 1. Tiêu đề hộp thông báo -->
<header class="header-notify-header">
<h3>Thông Báo Mới Nhận</h3>
</header>
<!-- 2. Danh sách các thông báo -->
<ul class="header-notify-list">
<!-- Item 1: Thông báo CHƯA ĐỌC (có thêm class --unread) -->
<li class="header-notify-item header-notify-item--unread">
<a href="#" class="header-notify-link">
<img src="https://placehold.co/100x100/ee4d2d/ffffff?text=Sale+50%25" alt="Ảnh thông báo" class="header-notify-img">
<div class="header-notify-info">
<span class="header-notify-name">🔥 Siêu Sale Giữa Tháng - Voucher Giảm Đến 50% Đã Vào Ví!</span>
<span class="header-notify-desc">Hàng ngàn deal sốc 1K, Freeship 0Đ mọi đơn hàng đang chờ bạn chốt đơn ngay hôm nay.</span>
</div>
</a>
</li>
<!-- Item 2: Thông báo CHƯA ĐỌC -->
<li class="header-notify-item header-notify-item--unread">
<a href="#" class="header-notify-link">
<img src="https://placehold.co/100x100/00bfa5/ffffff?text=Giao+Hang" alt="Ảnh thông báo" class="header-notify-img">
<div class="header-notify-info">
<span class="header-notify-name">Đơn hàng #261114SPVN của bạn đã được giao thành công</span>
<span class="header-notify-desc">Hãy đánh giá sản phẩm trước ngày 20/11 để nhận ngay 200 Shopee Xu nhé!</span>
</div>
</a>
</li>
<!-- Item 3: Thông báo ĐÃ ĐỌC (không có class --unread) -->
<li class="header-notify-item">
<a href="#" class="header-notify-link">
<img src="https://placehold.co/100x100/d0011b/ffffff?text=Mall" alt="Ảnh thông báo" class="header-notify-img">
<div class="header-notify-info">
<span class="header-notify-name">Xác thực tài khoản ShopeePay thành công</span>
<span class="header-notify-desc">Tài khoản ví điện tử của bạn đã được bảo mật 2 lớp. Tặng bạn mã giảm 20K cho đơn đầu tiên.</span>
</div>
</a>
</li>
</ul>
<!-- 3. Chân hộp thông báo: Nút Xem tất cả -->
<footer class="header-notify-footer">
<a href="#" class="header-notify-footer-btn">Xem tất cả</a>
</footer>
</div>
</li>
Bước 2: Viết CSS cho Hộp Thông Báo và Mũi Tên Tam Giác trong main.css
Mở file ./assets/css/main.css và bổ sung đoạn code dưới đây vào ngay sau phần CSS của .header-qr:
CSS
/* ./assets/css/main.css */
/* --- KHU VỰC HỘP THOẠI THÔNG BÁO (NOTIFICATION DROPDOWN) --- */
@keyframes headerNotifyGrowth {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
.header-notify {
position: absolute;
top: calc(100% + 0.8rem); /* Cách đáy chữ Thông Báo 8px */
right: 0; /* Căn sát mép phải của thẻ <li> Thông Báo */
width: 40.4rem; /* Chiều rộng chuẩn Shopee: 404px */
background-color: var(--white-color);
border: 1px solid var(--border-color);
border-radius: 2px;
box-shadow: 0 1px 3.125rem 0 rgba(0, 0, 0, 0.2);
z-index: 10;
cursor: default; /* Tránh việc con trỏ chuột biến thành bàn tay ở vùng trắng */
user-select: none; /* Không cho bôi đen văn bản thông báo */
/* Thiết lập tâm phóng to từ góc trên bên phải và chạy Animation */
transform-origin: calc(100% - 3.2rem) top;
animation: headerNotifyGrowth ease-in 0.2s;
will-change: opacity, transform;
display: none; /* Mặc định ẩn */
}
/* KÍCH HOẠT HIỂN THỊ KHI HOVER VÀO THẺ <LI> */
.nav-item--has-notify:hover .header-notify {
display: block;
}
/* 1. DÙNG ::BEFORE VẼ MŨI TÊN TAM GIÁC NHỌN CHỈ LÊN TRÊN */
.header-notify::before {
content: "";
display: block;
position: absolute;
right: 1.2rem; /* Căn chỉnh mũi tên nằm ngay dưới chữ Thông Báo */
top: -2.8rem; /* Đẩy mũi tên trồi lên trên nắp hộp */
border-width: 2rem 2.6rem; /* Tạo tam giác bè (Cao 20px, Rộng 26px) */
border-style: solid;
/* Chỉ giữ lại màu trắng ở cạnh Bottom, 3 cạnh còn lại trong suốt */
border-color: transparent transparent var(--white-color) transparent;
}
/* 2. DÙNG ::AFTER LÀM "CẦU NỐI VÔ HÌNH" CHỐNG MẤT HOVER */
.header-notify::after {
content: "";
display: block;
position: absolute;
right: 0;
top: -1.4rem;
width: 9rem; /* Đủ rộng để phủ kín chữ Thông Báo */
height: 1.6rem; /* Lấp đầy khe hở 8px */
}
/* Tiêu đề: "Thông Báo Mới Nhận" */
.header-notify-header {
height: 4rem;
background-color: var(--white-color);
}
.header-notify-header h3 {
color: var(--text-secondary);
margin: 0 0 0 1.2rem;
font-weight: 400;
font-size: 1.4rem;
line-height: 4rem; /* Căn giữa dọc đơn giản bằng cách cho line-height = height */
}
/* Danh sách các item thông báo */
.header-notify-list {
padding-left: 0;
}
.header-notify-item {
background-color: var(--white-color);
transition: background-color 0.15s ease;
}
/* Màu nền cam phấn nhạt đặc trưng cho thông báo CHƯA ĐỌC */
.header-notify-item--unread {
background-color: rgba(238, 77, 45, 0.08);
}
/* Khi rê chuột vào từng thông báo -> Đổi sang nền xám nhạt */
.header-notify-item:hover {
background-color: #f8f8f8;
}
.header-notify-link {
display: flex;
align-items: flex-start; /* Căn ảnh và chữ bám sát mép trên */
padding: 1.2rem;
width: 100%;
}
.header-notify-img {
width: 4.8rem; /* Rộng 48px */
height: 4.8rem; /* Cao 48px */
object-fit: cover; /* Giúp ảnh không bao giờ bị méo tỷ lệ */
flex-shrink: 0; /* Bắt buộc: Không cho Flexbox bóp méo chiều rộng của ảnh khi chữ quá dài */
border: 1px solid var(--border-color);
}
.header-notify-info {
margin-left: 1.2rem;
flex: 1;
}
.header-notify-name {
display: block;
font-size: 1.4rem;
color: var(--text-primary);
font-weight: 400;
line-height: 1.8rem;
margin-bottom: 0.4rem;
}
.header-notify-desc {
display: block;
font-size: 1.2rem;
color: var(--text-secondary);
line-height: 1.6rem;
}
/* Chân hộp thông báo: Nút "Xem tất cả" */
.header-notify-footer {
display: flex;
background-color: var(--white-color);
}
.header-notify-footer-btn {
color: var(--text-primary);
width: 100%;
padding: 1rem 0;
font-size: 1.4rem;
font-weight: 400;
text-align: center;
transition: background-color 0.15s ease;
}
.header-notify-footer-btn:hover {
background-color: #fafafa;
color: var(--shopee-primary);
}

4. Giải mã dòng code: 3 Chi tiết "nhỏ nhưng có võ"
-
Sự phân công hoàn hảo giữa
::beforevà::after:-
Ở Bài 04 (QR Code), chúng ta chỉ cần dùng
::beforelàm cầu nối vô hình vì hộp QR không có mũi tên. -
Sang Bài 05, hộp Thông Báo vừa cần Mũi tên tam giác, vừa cần Cầu nối vô hình. Một thẻ HTML có tối đa 2 phần tử giả là
::beforevà::after— chúng ta đã tận dụng triệt để cả hai:::beforevẽ mũi tên tam giác, còn::afterlàm cầu nối tàng hình chống mất Hover!
-
-
Thuộc tính cứu cánh
flex-shrink: 0trên thẻ ảnh (.header-notify-img):-
Khi đặt ảnh (
width: 48px) và khối chữ (.header-notify-info) chung trong mộtdisplay: flex, nếu đoạn văn bản thông báo quá dài, Flexbox sẽ tự động ép nhỏ chiều ngang của tấm ảnh lại (ví dụ từ48pxbị bẹp xuống còn20px) để nhường chỗ cho chữ! -
Dòng
flex-shrink: 0ra lệnh cho trình duyệt: "Tuyệt đối không được phép bóp nhỏ tấm ảnh này dưới bất kỳ hoàn cảnh nào!", kết hợp vớiobject-fit: covergiúp ảnh luôn vuông vức48x48px.
-
-
Màu nền
rgba(238, 77, 45, 0.08)cho.header-notify-item--unread:- Thay vì đi tìm một mã màu hồng nhạt mới, chúng ta lấy chính mã màu Cam Shopee (
#ee4d2dtương đươngrgb(238, 77, 45)) và hạ độ trong suốt (alpha) xuống chỉ còn 8% (0.08). Cách này giúp màu nền thông báo chưa đọc đồng điệu hoàn hảo với tông màu thương hiệu!
- Thay vì đi tìm một mã màu hồng nhạt mới, chúng ta lấy chính mã màu Cam Shopee (
5. Bài tập thực hành cuối bài & Hẹn gặp ở Bài 06!
Hãy lưu lại file và thưởng thức thành quả trên trình duyệt! Khi bạn đưa chuột lướt nhẹ vào mục "Thông Báo", cả một danh sách thông báo chuẩn Shopee với mũi tên nhọn sẽ bung ra mượt mà từ góc phải.
Thử thách khám phá dành cho bạn trước khi sang Bài 06:
-
Hãy thử vào
.header-notify::before, đổi dòngborder-color: transparent transparent var(--white-color) transparent;thànhborder-color: red green blue yellow;rồi rê chuột vào mục Thông báo. Bạn sẽ thấy ngay 4 hình tam giác 4 màu chụm lại với nhau — đây là cách trực quan nhất để hiểu bản chất thủ thuật vẽ tam giác bằng CSS Border! (Xem xong nhớ trả lại như cũ nhé). -
Hãy thêm tiếp 1 thẻ
<li class="header-notify-item">nữa vào danh sách với nội dung thông báo siêu dài (khoảng 5 dòng chữ), sau đó thử tắt thuộc tínhflex-shrink: 0ở.header-notify-imgtrong F12 để xem tấm ảnh bị bóp méo như thế nào.
Như vậy là sau 5 bài học đầu tiên, chúng ta đã hoàn thiện 100% thanh Top Navbar của Shopee! Ở Bài 06, chúng ta sẽ bước xuống tầng thứ hai của Header (header-with-search): Chèn Logo Shopee dạng Vector SVG sắc nét, dựng khung Tìm kiếm lớn kèm danh sách Lịch sử tìm kiếm thả xuống và cụm từ khóa Hot Trend!
All rights reserved