Tạm biệt JS cồng kềnh: Làm chủ <dialog>, Popover API và Native Accordion trong HTML hiện đại
Chào anh em lập trình viên trên Viblo!
Nếu đã từng làm Frontend từ những ngày đầu, chắc hẳn anh em không xa lạ gì với những "nỗi đau kinh điển":
- Đau đầu viết hàng chục dòng JavaScript chỉ để bắt sự kiện click ra ngoài thì ẩn popup (Click Outside / Light Dismiss).
- Viết hàm bắt phím
Escapeđể đóng modal, rồi lại hì hục code logic "bẫy con trỏ chuột" (Focus Trap) để người dùng khiếm thị dùng phímTabkhông bị nhảy ra ngoài trang nền. - Và đỉnh điểm là cuộc chiến
z-indexvô tiền khoáng hậu:z-index: 999,z-index: 9999, thậm chíz-index: 99999999nhưng chiếc dropdown menu vẫn bị một thẻdivcha có thuộc tínhoverflow: hiddennuốt chửng!
Để giải quyết những vấn đề đó, chúng ta từng phải cầu cứu đến hàng tá thư viện nặng nề: từ Bootstrap Modal, SweetAlert2, jQuery UI cho đến Tippy.js, Popper.js hay React Modal.

Nhưng đó là câu chuyện của quá khứ. HTML ngày nay đã trưởng thành vượt bậc. Với sự ra đời của thẻ <dialog>, thuộc tính popover, và tính năng Exclusive Accordion trên <details>, trình duyệt hiện đại đã chính thức hỗ trợ Native UI Components mạnh mẽ đến mức anh em có thể "vứt bỏ" hàng chục KB JavaScript mà website vẫn chạy mượt mà, chuẩn Accessibility (A11y).
Hãy cùng mình mổ xẻ tường tận bộ ba quyền lực này ngay sau đây!
1. Thẻ <dialog>: Vũ khí tối thượng xóa sổ Modal JavaScript
Trước đây, để tạo một chiếc Popup/Modal chuẩn chỉnh, cấu trúc HTML của anh em thường trông như thế này:
<!-- CÁCH CŨ: Tốn công và dễ lỗi -->
<div class="modal-backdrop"></div>
<div class="modal-wrapper">
<div class="modal-content">
<button class="close-btn">×</button>
<p>Nội dung modal...</p>
</div>
</div>
Sau đó là hàng tá CSS canh giữa (position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%)) và hàng đống JS để quản lý đóng mở.
Giờ đây, tất cả được gói gọn trong 1 thẻ HTML duy nhất:
<!-- CÁCH HIỆN ĐẠI: Thuần HTML -->
<dialog id="myModal">
<h2>Chào mừng bạn đến với Viblo!</h2>
<p>Đây là native modal được dựng hoàn toàn bằng HTML5.</p>
<button id="closeBtn">Đóng lại</button>
</dialog>
<button id="openBtn">Mở Modal</button>
Và chỉ cần 3 dòng JavaScript tinh gọn:
const modal = document.getElementById("myModal");
document.getElementById("openBtn").addEventListener("click", () => modal.showModal());
document.getElementById("closeBtn").addEventListener("click", () => modal.close());
+-------------------------------------------------------------+
| BROWSER VIEWPORT |
| |
| +-----------------------------------------------------+ |
| | ::backdrop (Lớp mờ) | |
| | | |
| | +---------------------------------+ | |
| | | <dialog> MODAL | | |
| | | (Nằm trên Top Layer độc lập) | | |
| | | - Tự động căn giữa màn hình | | |
| | | - Bấm ESC tự động đóng | | |
| | | - Focus Trap hoàn hảo | | |
| | +---------------------------------+ | |
| +-----------------------------------------------------+ |
| |
+-------------------------------------------------------------+
Cơ chế hiển thị trên Top Layer của thẻ <dialog>
Những "siêu năng lực" tích hợp sẵn của <dialog>:
showModal()và khái niệm Top Layer: Khi gọimodal.showModal(), trình duyệt sẽ đưa phần tử này lên một tầng đặc biệt gọi là Top Layer. Tầng này nằm trên tất cả các phần tử khác trong DOM tree. Nghĩa là anh em hoàn toàn không cần dùng đếnz-index, và nó sẽ không bao giờ bị cắt cụt bởi các thẻ cha cóoverflow: hidden!- Pseudo-element
::backdrop: Trình duyệt tự sinh ra lớp phủ làm mờ phía sau. Anh em có thể tha hồ style chỉ bằng CSS:dialog::backdrop { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); /* Làm mờ background siêu xịn */ } - Focus Trap và phím ESC tích hợp sẵn: Khi mở modal, con trỏ focus tự động nhảy vào nút đầu tiên bên trong dialog. Người dùng nhấn
Tabsẽ chỉ di chuyển vòng tròn bên trong modal mà không bị lọt ra ngoài trang web. Đặc biệt, người dùng nhấn phímEscapelà modal tự động đóng mà không cần bắt sự kiệnkeydown! - Đóng modal không cần JS bằng Form
method="dialog":
Khi người dùng click bất kỳ nút nào trong form này,<dialog id="confirmDialog"> <form method="dialog"> <p>Bạn có chắc chắn muốn xóa bài viết này?</p> <button value="cancel">Hủy</button> <button value="confirm" class="btn-danger">Đồng ý</button> </form> </dialog><dialog>sẽ tự động đóng lại và truyền giá trị của nút bấm vào thuộc tínhmodal.returnValue!
2. Popover API: Khai tử Popper.js & Tooltip Libraries
Nếu <dialog> dành cho Modal toàn màn hình, thì Popover API chính là "chân ái" cho các thành phần như: Dropdown menu, Tooltip, Toast notification, hay Popup bộ lọc.
Trước đây, để làm một dropdown menu tự đóng khi click ra ngoài, anh em phải viết script tính toán tọa độ và lắng nghe sự kiện click toàn trang.
Giờ đây, với Popover API (đã được hỗ trợ 100% trên tất cả các trình duyệt hiện đại), anh em làm được điều đó mà KHÔNG CẦN DÙNG 1 DÒNG JAVASCRIPT NÀO!
<!-- Nút kích hoạt Popover -->
<button popovertarget="userMenu">Tài khoản của tôi ▾</button>
<!-- Nội dung Popover -->
<div id="userMenu" popover>
<ul>
<li><a href="/profile">Hồ sơ cá nhân</a></li>
<li><a href="/settings">Cài đặt</a></li>
<li><a href="/logout">Đăng xuất</a></li>
</ul>
</div>
/* Style cơ bản */
[popover] {
padding: 16px;
border-radius: 8px;
border: 1px solid #e2e8f0;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
Tại sao Popover API lại "bá đạo"?
- Cơ chế Light Dismiss tự động: Nhấp chuột ra bất kỳ vị trí nào bên ngoài popover, hoặc bấm phím
ESC, popover sẽ tự động biến mất. - Tự động quản lý Top Layer: Giống như
<dialog>, popover cũng hiển thị ở Top Layer, vĩnh biệt nỗi loz-index. - Hai chế độ linh hoạt:
popover="auto"(Mặc định): Tự đóng khi click ra ngoài; khi một popover khác mở ra, popover hiện tại sẽ tự tắt.popover="manual": Dành cho Toast notification hoặc Tooltip cố định, chỉ đóng khi được kích hoạt cụ thể.
3. <details name="...">: Chiếc Accordion không tốn một giọt JS
Thẻ <details> và <summary> trong HTML đã xuất hiện từ lâu để làm các khối đóng/mở nội dung (Collapsible). Nhưng trước đây nó có một điểm yếu chí mạng: Không thể làm Accordion tương hỗ (Exclusive Accordion). Tức là khi mở mục này thì mục kia không tự co lại được, buộc dev phải nhảy vào viết JavaScript.
Nhưng kể từ phiên bản HTML cải tiến gần đây, thuộc tính name đã chính thức cập bến thẻ <details>:
<!-- CHỈ CẦN DÙNG CHUNG THUỘC TÍNH NAME -->
<div class="faq-container">
<details name="faq-group" open>
<summary>Viblo là nền tảng gì?</summary>
<p>Viblo là cộng đồng chia sẻ kiến thức công nghệ thông tin hàng đầu Việt Nam.</p>
</details>
<details name="faq-group">
<summary>Viết bài trên Viblo có được nhận quà không?</summary>
<p>Có, bạn có thể tham gia các sự kiện như MayFest hoặc Khai bút đầu xuân.</p>
</details>
<details name="faq-group">
<summary>Tôi có thể dùng HTML thuần thay React được không?</summary>
<p>Hoàn toàn được với các dự án vừa và nhỏ, tối ưu hiệu năng vượt trội!</p>
</details>
</div>
/* Tùy biến biểu tượng mũi tên mượt mà */
details summary {
cursor: pointer;
padding: 12px;
font-weight: 600;
background: #f8fafc;
border-radius: 6px;
}
details[open] summary {
background: #e2e8f0;
}
Khi người dùng bấm mở thẻ <details> thứ hai, thẻ thứ nhất sẽ tự động co lại ngay lập tức, hệt như cách hoạt động của các nút radio button! Không cần state, không cần React hooks, không cần event listener.
4. Bảng so sánh: Giải pháp Native HTML vs Thư viện JavaScript truyền thống
Để thấy rõ sự khác biệt, hãy cùng đặt lên bàn cân so sánh giữa việc dùng component Native và các thư viện JavaScript:
| Tiêu chí | Dùng thư viện JS (SweetAlert, Bootstrap...) | Dùng HTML5 Native (<dialog>, Popover) |
|---|---|---|
| Kích thước Bundle (Bundle Size) | Tăng thêm từ 30KB - 150KB (JS + CSS) | 0 KB (Trình duyệt tích hợp sẵn) |
Cuộc chiến z-index |
Dễ bị đè layer nếu CSS cha có overflow:hidden |
Miễn nhiễm 100% nhờ cơ chế Top Layer |
| Hỗ trợ Trợ năng (Accessibility - A11y) | Phải tự gán aria-modal, aria-expanded, code Focus Trap |
Tự động hỗ trợ chuẩn WAI-ARIA từ lõi trình duyệt |
| Hiệu năng render (Performance) | Tốn RAM/CPU parse JS, dễ gây giật khung hình trên mobile | Chạy bằng mã nguồn C++ của engine trình duyệt, siêu mượt |
| Bắt sự kiện Click Outside / ESC | Phải viết code lắng nghe window.onclick |
Tích hợp sẵn mặc định |
5. Kinh nghiệm thực chiến: Làm Animation mượt mà cho Native Component
Nhiều anh em chê <dialog> hay popover vì khi bật/tắt nó nhảy ra đột ngột, không có hiệu ứng Fade-in hay Slide-down đẹp mắt như thư viện JS.
Thực tế, với các thuộc tính CSS hiện đại như @starting-style và transition-behavior: allow-discrete, anh em hoàn toàn có thể tạo animation mượt như nhung cho <dialog>:
/* Style animation cho <dialog> */
dialog {
opacity: 0;
transform: translateY(-20px) scale(0.95);
transition: opacity 0.3s ease, transform 0.3s ease, overlay 0.3s allow-discrete, display 0.3s allow-discrete;
}
/* Trạng thái khi Modal mở */
dialog[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
/* Điểm khởi đầu của Animation khi chuyển từ display:none sang open */
@starting-style {
dialog[open] {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
}
/* Hiệu ứng mờ dần cho Backdrop */
dialog::backdrop {
background-color: rgba(0, 0, 0, 0);
transition: background-color 0.3s ease, overlay 0.3s allow-discrete, display 0.3s allow-discrete;
}
dialog[open]::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
@starting-style {
dialog[open]::backdrop {
background-color: rgba(0, 0, 0, 0);
}
}
Chỉ với đoạn CSS trên, chiếc modal native của anh em sẽ phóng to và mờ dần cực kỳ hiện đại mỗi khi gọi .showModal(), không thua kém bất kỳ thư viện UI hào nhoáng nào!
Lời kết: Đã đến lúc tư duy "HTML-First" quay trở lại
Trong nhiều năm qua, chúng ta đã quá quen với việc dựa dẫm vào JavaScript và các framework để giải quyết mọi bài toán giao diện. Chúng ta sẵn sàng cài đặt một package hàng chục KB chỉ để hiển thị một chiếc tooltip hay một bảng thông báo nhỏ.
Sự phát triển vượt bậc của các tiêu chuẩn web hiện đại với <dialog>, Popover API và Accordion thuần chứng minh một xu hướng rõ ràng: Nền tảng web đang ngày càng mạnh mẽ hơn, thông minh hơn và dần thay thế những giải pháp chắp vá từ JavaScript.
Việc tận dụng tối đa sức mạnh của HTML thuần không chỉ giúp giảm dung lượng tải trang, tăng điểm Google PageSpeed (Core Web Vitals) mà còn giúp code của anh em sạch sẽ, dễ bảo trì và trường tồn với thời gian.
Anh em đã thử áp dụng các tính năng HTML native này vào dự án thực tế nào chưa? Hãy cùng để lại bình luận và chia sẻ trải nghiệm bên dưới nhé!
All Rights Reserved