Dialog

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, tombol inert, atau pengendali Esc.
  • JANGAN membukanya dengan show() atau atribut open.
  • 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: hidden atau position: fixed di html atau body.
/* 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-labelledby yang mengarah ke judulnya, atau aria-label jika tidak ada judul yang terlihat.
  • LAKUKAN penempatan autofocus pada kontrol utama atau kolom pertama.
  • LAKUKAN penempatan autofocus pada 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" dan commandfor di <button>. Lihat Kontrol Dialog dan Popover Deklaratif.
  • GUNAKAN dialog.showModal() saat membuka dari skrip.

Penutup

  • LAKUKAN penutupan dengan tombol <form method="dialog"> atau command="close".
  • LAKUKAN pemilihan closedby dengan sengaja: any (tutup ringan dan Esc), closerequest (hanya Esc, default modal), atau none (hanya tutup yang dikontrol developer).
  • GUNAKAN command="request-close" atau requestClose() untuk tombol Batal yang harus mematuhi logika perubahan yang belum disimpan. Mereka mengaktifkan cancel terlebih dahulu dan mengabaikan closedby.
  • LAKUKAN: Perlakukan closedby sebagai progressive enhancement. Esc dan gestur kembali di perangkat seluler sudah menutup modal tanpa tombol tersebut.

Mengaitkan ke penutupan

  • LAKUKAN pembacaan dialog.returnValue di pemroses close untuk mempelajari tombol mana yang digunakan, lalu reset dengan dialog.returnValue = ''. Esc dan 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 cancel untuk menangkap Esc dan request-close, tetapi jangan mengandalkan preventDefault() untuk penekanan Esc berulang 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 cancel dicegah: tampilkan pesan yang terlihat dan referensikan dengan aria-describedby dari 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, bukan display atau hidden) 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> (auto atau manual) untuk panel lapisan atas, dan dialog.show() jika Anda tidak menginginkan lapisan atas.
  • LAKUKAN penyertaan tombol tutup yang terlihat di setiap dialog non-modal.

Latar belakang dan penataan gaya

Aksesibilitas

  • JANGAN menambahkan role="dialog", aria-modal, perangkap fokus manual, atau pengalihan inert ke 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.