Forge Dialog: hộp thoại dựng trên thẻ <dialog> gốc, không còn phải dò z-index
Một trang quản trị cần hỏi "bạn có chắc muốn xoá?". Hàm confirm sẵn có của trình duyệt làm được nhưng chặn cả trang và không đổi được giao diện; thư viện thay thế thì thường kéo theo cả framework. Forge Dialog là cách chúng tôi giải bài toán đó: không phụ thuộc runtime, dựng trên thẻ dialog gốc, và bộ nối cho React, Vue, Svelte nằm ngay trong cùng một gói.
Tình huống quen thuộc: một trang quản trị có nút xoá, và trước khi xoá thật thì cần hỏi lại người dùng một câu.
Cách nhanh nhất là dùng hàm confirm() sẵn có của trình duyệt. Nó chạy được ngay, không cần cài gì. Nhưng nó có ba vấn đề mà ai từng dùng thật đều gặp: nó chặn toàn bộ trang cho tới khi người dùng bấm, nên mọi thứ khác đóng băng theo; nó không đổi được giao diện, nên hiện ra một hộp hệ thống trông lạc lõng giữa sản phẩm; và trên điện thoại nó hiển thị kèm tên miền của trang, thứ trông giống cảnh báo bảo mật hơn là một câu hỏi bình thường.
Lối thoát thông thường là cài một thư viện hộp thoại. Nhưng phần lớn lựa chọn hoặc gắn với một framework cụ thể, hoặc gộp mọi tính năng vào một gói — bạn chỉ cần một hộp xác nhận nhưng phải tải về cả phần dựng ngăn kéo, bảng lệnh và trình chiếu ảnh.
Forge Dialog là cách chúng tôi giải bài toán đó cho các sản phẩm của mình, và mở mã nguồn theo giấy phép MIT. Bài này nói về ba quyết định thiết kế đứng sau nó.
Quyết định 1 — dựng trên thẻ `<dialog>` gốc, không tự vẽ lớp phủ
Đây là khác biệt kỹ thuật đáng nói nhất, và nó giải quyết một vấn đề mà bất kỳ ai từng làm modal đều đã mất thời gian vì nó: z-index.
Cách làm truyền thống là tạo một thẻ div phủ lên trang rồi đẩy nó lên trên bằng z-index. Vấn đề là con số đó chỉ có nghĩa trong phạm vi ngữ cảnh xếp lớp của phần tử cha. Một thanh điều hướng cố định, một thẻ cha có transform hay filter, một thư viện bản đồ đặt z-index: 9999 — bất kỳ thứ nào cũng có thể che mất hộp thoại của bạn, hoặc cắt nó khi nó tràn ra ngoài khung. Cách chữa thường thấy là tăng dần con số cho tới khi nó chịu hiện, rồi vài tháng sau có người khác làm y hệt với một thành phần khác.
Thẻ <dialog> gốc của trình duyệt bỏ hẳn cuộc đua đó. Khi mở bằng phương thức dành cho modal, trình duyệt đưa nó lên tầng trên cùng — một lớp nằm ngoài luồng xếp lớp thông thường của trang. Không phần tử nào trong trang che được nó, bất kể z-index là bao nhiêu, và nó không bị cắt bởi thẻ cha nào. Kèm theo đó là hai thứ trình duyệt lo hộ: phần còn lại của trang tự động bị đánh dấu trơ với thao tác, và phím Escape đóng hộp thoại theo đúng thói quen hệ điều hành.
Điểm thực tế: khi hộp thoại bị che hoặc bị cắt trong một bố cục phức tạp, đó gần như luôn là vấn đề của cách dựng chứ không phải của thư viện. Chuyển sang thẻ gốc là bỏ hẳn nhóm lỗi đó thay vì đi chữa từng ca.
Quyết định 2 — trả về promise thay vì chặn trang
Hàm confirm() của trình duyệt đóng băng mọi thứ. Forge Dialog thay bằng hàm trả về một lời hứa, nên phần còn lại của giao diện vẫn chạy bình thường trong lúc hộp thoại mở:
const ok = await confirm("Xoá mục này?", { title: "Xác nhận" })— trả vềtruehoặcfalse, và trả vềfalsekhi người dùng bấm Escape hoặc bấm ra ngoài.const ten = await prompt("Tên của bạn?", { validate: (v) => v.trim() ? true : "Chưa nhập tên" })— trả về chuỗi đã nhập, hoặcnullnếu huỷ. Hàmvalidatecho phép kiểm tra ngay trong hộp thoại thay vì đóng lại rồi mới báo lỗi.await alert("Đã lưu.", { title: "Thành công" })— hộp một nút.
Với những trường hợp phức tạp hơn, hàm open() trả về một đối tượng có close(), update() và whenClosed(), nên hộp thoại có thể đổi nội dung khi đang mở — hữu ích cho các bước tải dữ liệu.
Khác biệt nghe nhỏ nhưng đổi hẳn cảm giác dùng: với hàm cũ, ảnh đang tải sẽ dừng, đồng hồ đếm ngược sẽ đứng, và hoạt ảnh sẽ khựng cho tới khi người dùng bấm nút. Với hàm mới thì không.
Quyết định 3 — mười bốn đường nhập, trả tiền cho phần mình dùng
Phần lớn thư viện hộp thoại phát hành một gói duy nhất: nhập vào là nhận tất cả. Nếu sản phẩm của bạn chỉ cần hộp xác nhận, bạn vẫn tải về phần dựng ngăn kéo, bảng lệnh và trình chiếu ảnh.
Forge Dialog tách theo tính năng. Cần gì nhập nấy:
forgedialog/alert,forgedialog/confirm,forgedialog/prompt— ba hộp thoại cơ bản, mỗi đường nhập chỉ kéo theo phần của nó.forgedialog/core— nhân đầy đủ, cho trường hợp cần hàmopen()và tuỳ biến sâu.forgedialog/workflows,/presentation,/interactions,/animations,/appearance— các nhóm tính năng nâng cao, chỉ nhập khi dùng.
Phần giao diện cũng tách tương tự: thay vì một tệp CSS duy nhất, có forgedialog/style/core.css cho phần lõi và các tệp riêng cho biểu mẫu, thông báo nổi, trình chiếu ảnh, bảng lệnh và phần kéo di chuyển.
Bộ nối framework nằm trong cùng gói — và vì sao lần này chúng tôi làm khác
Với Forge Select, chúng tôi phát hành wrapper cho React và Vue thành hai gói npm riêng. Với Forge Dialog thì không: bộ nối nằm ngay trong gói chính dưới dạng đường nhập con.
- React:
import { useDialog } from "forgedialog/react" - Vue:
import { useDialog } from "forgedialog/vue" - Svelte:
import ... from "forgedialog/svelte" - Không dùng framework nào:
forgedialog/web-component
Lý do đổi cách làm rất thực tế: gói riêng nghĩa là có hai số phiên bản phải giữ khớp nhau. Mỗi lần gói chính lên phiên bản, gói wrapper phải phát hành theo, và người dùng phải nhớ nâng cả hai. Gộp vào một gói thì không còn chuyện lệch phiên bản, và React với Vue được khai báo là phụ thuộc *tuỳ chọn* — cài Forge Dialog trong một dự án không dùng framework nào cũng không kéo theo gì.
Mặc định an toàn với nội dung
Bất kỳ thành phần nào nhận nội dung rồi hiển thị lên trang đều là một bề mặt tấn công tiềm năng. Forge Dialog chọn mặc định nghiêm:
- Chuỗi truyền vào
contentluôn hiện ra dạng chữ. Nếu nó chứa thẻ HTML, người dùng sẽ thấy đúng đoạn thẻ đó chứ không phải kết quả dựng ra. - Muốn chèn HTML thật thì phải truyền kèm bộ lọc:
open({ html: noiDung, sanitizeHtml: (h) => DOMPurify.sanitize(h) }). Không có bộ lọc thì không chèn được. - Trường
unsafeHtmlvẫn tồn tại cho nội dung do chính hệ thống sinh ra và tin được, nhưng phải gọi đúng cái tên đó. Đây là lựa chọn cố ý: một trường tênunsafeHtmlsẽ không bị ai dùng nhầm trong lúc vội.
Không chỉ hộp thoại
Cùng một nhân còn dựng được những thành phần khác vốn hay bị làm lại từ đầu ở mỗi dự án: ngăn kéo trượt từ cạnh màn hình, bottom sheet cho điện thoại với thao tác vuốt để đóng, xem ảnh phóng to, thông báo nổi, bảng lệnh gọi bằng bàn phím, và trình hướng dẫn nhiều bước có nhớ trạng thái để người dùng quay lại đúng chỗ đang dở.
Phần tuỳ biến giao diện cũng làm theo từng hộp thoại chứ không phải toàn cục: màu và độ mờ tách riêng cho nền, tiêu đề, nội dung và viền; bo góc đặt được cho từng góc; đổ bóng dựng từ góc chiếu, khoảng cách và độ nhoè thay vì phải viết chuỗi CSS. Hộp thoại kéo di chuyển được bằng chuột hoặc bàn phím, giới hạn trong khung nhìn, và nhớ vị trí cho lần mở sau.
Về khả năng tiếp cận, hai điểm đáng nói: các hiệu ứng chuyển động tôn trọng thiết lập giảm chuyển động của hệ điều hành, và hộp thoại kéo được bằng phím mũi tên chứ không chỉ bằng chuột.
Trạng thái hiện tại, nói thẳng
Forge Dialog đang ở phiên bản 0.7.0, phát hành lên npm ngày 1/9/2026. Đây là bản đầu tiên được đưa lên npm, dù trong repo đã có lịch sử từ 0.4. Con số phiên bản dưới 1.0 mang đúng nghĩa của nó: giao diện lập trình vẫn có thể đổi giữa các bản nhỏ, và chúng tôi ghi lại mọi thay đổi trong tệp CHANGELOG kèm hướng dẫn chuyển đổi.
Nếu bạn cần một thư viện đã ổn định nhiều năm cho một hệ thống không đụng tới nữa, đây chưa phải lựa chọn. Nếu bạn đang dựng sản phẩm và muốn một thành phần không phụ thuộc framework, không kéo theo gì, và có thể đọc hết mã nguồn trong một buổi chiều, thì nó đáng thử.
Thử trong hai phút
- Xem bản chạy thật và tài liệu tại forgedialog.konexforge.com.
- Cài bằng
npm install forgedialog, hoặc dùng thẳng qua thẻ<script>nếu dự án chưa có bước dựng. - Mã nguồn đầy đủ trên GitHub, giấy phép MIT — dùng được cho cả dự án thương mại, không cần xin phép, không cần ghi công.
Forge Dialog cùng các dự án mã nguồn mở khác của chúng tôi được liệt kê đầy đủ — phiên bản, giấy phép, cách đóng góp — ở trang Cộng đồng. Đây cũng là loại thành phần chúng tôi tái sử dụng trong lớp Development của các dự án cần một lớp giao diện đáng tin mà không phải xây lại từ đầu.
Bài viết liên quan
Forge Select: bản thay thế Select2 zero-dependency, framework-agnostic mã nguồn mở
Select2 đã phục vụ cộng đồng web nhiều năm, nhưng dựa trên jQuery — một ràng buộc ngày càng khó chấp nhận với stack hiện đại. Forge Select là component select/combobox chúng tôi tự xây: toàn bộ core chỉ 5 file, zero runtime dependency, nhưng đủ tính năng cho sản phẩm thật — virtual scroll, tree select, tags, và accessibility đầy đủ.
Autumn Note: vì sao chúng tôi tự xây một rich-text editor zero-dependency, mã nguồn mở
Hầu hết hệ thống nội bộ — CMS, admin portal, knowledge base — đều cần một ô soạn thảo văn bản có định dạng. Thay vì kéo theo jQuery hoặc một thư viện thương mại nặng, chúng tôi xây Autumn Note: editor WYSIWYG thuần ES2022, zero dependency, mã nguồn mở MIT.
KonexForge Themes: bộ theme VS Code với thẩm mỹ 'forge' — không phải một bản sao One Dark khác
Engineer nhìn vào editor cả ngày, nhưng thị trường theme VS Code gần như bão hòa bởi các biến thể gần giống hệt One Dark Pro. KonexForge Themes là bộ 4 theme mã nguồn mở chúng tôi tự làm — không phải để bán, mà vì bản sắc thị giác riêng cũng là một phần của văn hóa kỹ thuật.