Ringkasan
Gunakan elemen <dialog> native untuk dialog modal dan non-modal. Elemen ini menyediakan semantik aksesibilitas, penanganan fokus, rendering lapisan atas, dan penanganan Esc yang harus dibuat ulang secara manual oleh overlay kustom.
Memilih jenis overlay
Pilih aturan pertama yang cocok. Jika tidak yakin, gunakan Aturan 1.
Aturan 1: Overlay mengganggu pengguna dan memblokir halaman (default)
Gunakan untuk konfirmasi, formulir, login, tindakan destruktif, dan permintaan "modal", "dialog", "lightbox", atau "pop-up" yang tidak menyatakan sebaliknya.
Gunakan <dialog> yang dibuka sebagai modal, secara deklaratif dengan command="show-modal" (lihat Kontrol Dialog dan Popover Deklaratif) atau dari skrip dengan showModal().
<button type="button" command="show-modal" commandfor="confirm">Delete</button>
<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this item?</h2>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
Browser menyediakan: halaman inert, pembatasan fokus, lapisan atas, ::backdrop, Esc dan penutupan gestur kembali, serta pengembalian fokus ke pemanggil.
- JANGAN tambahkan
role="dialog",aria-modal, perangkap fokus, tombolinert, atau pengendaliEsc. - JANGAN membukanya dengan
show()atau atributopen. - LAKUKAN penghentian scroll halaman di belakangnya dengan CSS di bawah. Lihat Mencegah Halaman di Bawah Dialog Modal Di-scroll untuk mengetahui detailnya.
- JANGAN mengunci scroll dengan
overflow: hiddenatauposition: fixeddihtmlataubody.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
overscroll-behavior: contain;
}
Tidak untuk menu atau tooltip: lihat Aturan 3.
Aturan 2: Pengguna terus bekerja di halaman saat panel terbuka
Gunakan untuk kotak temukan, pemeriksa, palet, dan panel tulis yang masih berupa dialog secara semantik.
Gunakan <dialog popover> untuk panel lapisan atas dengan penutupan popover (auto menutup ringan, manual tidak). Gunakan dialog.show() hanya jika panel tidak boleh berada di lapisan atas.
<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
<h2 id="find-title">Find</h2>
<input type="search" aria-label="Search">
<button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>
Halaman tetap interaktif dan tidak dibuat inert.
- LAKUKAN penyertaan tombol tutup yang terlihat.
- JANGAN gunakan
showModal(), yang memblokir halaman.
Bukan untuk memblokir konfirmasi: lihat Aturan 1.
Aturan 3: Overlay bukan dialog
Gunakan untuk menu, dropdown, tooltip, kartu pengarah, dan toast.
Gunakan elemen semantik dengan atribut popover, bukan <dialog>.
<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
<li><a href="/rename">Rename</a></li>
</ul>
Pilih cara menutupnya
Setelan defaultnya bergantung pada cara dialog dibuka. Jika closedby tidak ada atau tidak valid, showModal() akan berperilaku sebagai closerequest dan cara pembukaan lainnya akan berperilaku sebagai none.
- Hanya gestur Esc dan kembali (default modal): tidak menetapkan apa pun, atau
closedby="closerequest". - Klik juga di luar untuk menutup: gunakan
closedby="any". - Hanya developer yang menutupnya (misalnya formulir dengan perubahan yang belum disimpan): gunakan
closedby="none"dan tambahkan jalur konfirmasi. Lihat Penutupan di bawah.
Panduan
Label aksesibilitas dan fokus awal
- LAKUKAN penamaan setiap dialog dengan
aria-labelledbyyang mengarah ke judulnya, atauaria-labeljika tidak ada judul yang terlihat. - LAKUKAN penempatan
autofocuspada kontrol utama atau kolom pertama. - LAKUKAN penempatan
autofocuspada tindakan yang aman (seperti Batal atau Simpan) dalam konfirmasi destruktif. - JANGAN menulis kode untuk mengembalikan fokus saat ditutup. Browser akan mengembalikannya ke pemanggil.
Pembukaan
- LAKUKAN pembukaan secara deklaratif dengan
command="show-modal"dancommandfordi<button>. Lihat Kontrol Dialog dan Popover Deklaratif. - GUNAKAN
dialog.showModal()saat membuka dari skrip.
Penutup
- LAKUKAN penutupan dengan tombol
<form method="dialog">ataucommand="close". - LAKUKAN pemilihan
closedbydengan sengaja:any(tutup ringan danEsc),closerequest(hanyaEsc, default modal), ataunone(hanya tutup yang dikontrol developer). - GUNAKAN
command="request-close"ataurequestClose()untuk tombol Batal yang harus mematuhi logika perubahan yang belum disimpan. Mereka mengaktifkancancelterlebih dahulu dan mengabaikanclosedby. - LAKUKAN: Perlakukan
closedbysebagai progressive enhancement.Escdan gestur kembali di perangkat seluler sudah menutup modal tanpa tombol tersebut.
Mengaitkan ke penutupan
- LAKUKAN pembacaan
dialog.returnValuedi pemrosescloseuntuk mempelajari tombol mana yang digunakan, lalu reset dengandialog.returnValue = ''.Escdan permintaan penutupan lainnya tidak menetapkan nilai, dan browser berbeda dalam hal apakah browser tersebut menghapus nilai sebelumnya, sehingga tanpa reset, nilai yang sudah tidak berlaku dapat bocor. Perlakukan''sebagai "ditutup tanpa memilih". - GUNAKAN
canceluntuk menangkapEscdanrequest-close, tetapi jangan mengandalkanpreventDefault()untuk penekananEscberulang tanpa aktivasi pengguna. - LAKUKAN penggunaan
closedby="none"dengan langkah konfirmasi eksplisit untuk formulir yang belum disimpan. - LAKUKAN: beri tahu pengguna alasan dialog tetap terbuka saat
canceldicegah: tampilkan pesan yang terlihat dan referensikan denganaria-describedbydari kontrol yang menerima fokus (seperti Hapus), sehingga pembaca layar membacanya saat fokus berpindah. Tanpa itu, fokus yang berpindah ke tombol baru hanya akan mengumumkan nama tombol tersebut. - JANGAN biarkan konfirmasi memindahkan kontrol di bawah kursor. Cadangkan ruang pesan (sembunyikan dengan
visibility, bukandisplayatauhidden) dan tambahkan tombol destruktif di tempat tombol yang ada tidak berpindah (misalnya, pertama dalam baris rata kanan), sehingga klik kedua di tempat yang sama tidak dapat menghapus perubahan pengguna.
Dialog non-modal
- GUNAKAN
<dialog popover>(autoataumanual) untuk panel lapisan atas, dandialog.show()jika Anda tidak menginginkan lapisan atas. - LAKUKAN penyertaan tombol tutup yang terlihat di setiap dialog non-modal.
Latar belakang dan penataan gaya
- LAKUKAN penataan gaya
::backdropdan gunakandialog:modaluntuk menargetkan gaya khusus modal. - LAKUKAN serahkan animasi ke Menganimasikan Elemen Ke dan Dari Lapisan Atas, menarik ke Membuat Elemen yang Dapat Ditarik, dan mengubah induk ke UI Lapisan Atas Persisten.
- JANGAN tetapkan
displaydidialogtanpa syarat. Atribut ini menggantikan status tertutup. Tentukan cakupannya dengandialog[open].
Aksesibilitas
- JANGAN menambahkan
role="dialog",aria-modal, perangkap fokus manual, atau pengalihaninertke modal native. - LAKUKAN penggunaan
<button>native dengan label aksesibilitas untuk kontrol tutup. - LAKUKAN penggunaan satu level judul yang konsisten untuk judul dialog.
Strategi penggantian
closedby (dialog-closedby)
Tanpa closedby, modal masih ditutup dengan Esc dan gestur kembali; hanya klik di luar yang hilang, jadi ini adalah degradasi halus. closedby="none" diabaikan dengan cara yang sama: Esc mengaktifkan cancel, lalu menutup dialog. Simpan penjaga perubahan yang belum disimpan di pengendali cancel, bukan hanya di closedby="none", sehingga juga melindungi saat closedby tidak ada. Jika target Dasar Pengukuran Anda memerlukan penutupan ringan, tambahkan ini jika closedby tidak ada:
if (!('closedBy' in HTMLDialogElement.prototype)) {
// Test the pointer position, not event.target: the <dialog> is also the target for its own
// padding and the gaps between its children, so a target check would close on those clicks
const outside = (e, dialog) => {
const r = dialog.getBoundingClientRect();
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
};
for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
let pressedOutside = false;
dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
// Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
}
}
overscroll-behavior
Perlakukan pembatasan scroll sebagai progressive enhancement. Jangan mendeteksi fitur ini: @supports melaporkan properti sebagai didukung meskipun penampung yang tidak dapat di-scroll mengabaikannya. Jika tidak diterapkan, dialog masih berisi scroll setiap kali kontennya sendiri meluap, dan showModal() masih membuat halaman menjadi tidak aktif. Jangan beralih ke overflow: hidden atau position: fixed di html atau body, yang akan menghapus scrollbar root dan menggeser tata letak.
Perintah Invoker (command / commandfor)
Jika target Dasar Pengukuran Anda tidak menyertakan Invoker Commands, pertahankan markup yang sama dan muat polyfill secara bersyarat (pengalaman yang sama, sebagai progressive enhancement). Deteksi di antarmuka dan jangan pernah memuatnya tanpa syarat:
// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill'); // npm install invokers-polyfill
}
Hanya Aturan 2 dan 3 yang menggunakan Popover.
Penggantian & dukungan browser untuk Popover
Popover API sebagian besar merupakan progressive enhancement, tetapi perilaku penentuannya — promosi lapisan atas, penutupan ringan, dan pemanggilan popovertarget — tidak memiliki padanan khusus CSS. Browser lama memerlukan polyfill, atau penggantian manual jika Anda tidak ingin mengirimkannya.
Polyfill: Untuk mendukung atribut popover di browser lama, muat @oddbird/popover-polyfill secara bersyarat. WAJIB: Deteksi fitur dengan memeriksa properti popover di HTMLElement.prototype, dan muat polyfill hanya jika dukungan native tidak ada — JANGAN memuatnya tanpa syarat.
Dengan bundler atau peta impor:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Tanpa bundler, impor dari CDN di dalam <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Peringatan gaya: Polyfill tidak dapat menentukan pseudo-class :popover-open yang sebenarnya, sehingga menerapkan class .\:popover-open. WAJIB: Gabungkan keduanya dengan :is() atau :where(). Jika tidak, browser yang tidak memiliki :popover-open akan menghapus seluruh aturan:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Atau, untuk penggantian lama tanpa polyfill, gunakan position: fixed dan hitung koordinat secara manual melalui getBoundingClientRect() atau andalkan pemosisian default dengan inset: auto jika hal itu dapat diterima untuk kasus penggunaan.