Membuat menu dropdown
Panduan ini membahas menu pop-up apa pun: dropdown, konteks, tambahan, dan panel menu yang membukanya. Contoh yang diberikan adalah menu perintah aplikasi, dengan item yang mengubah antarmuka (buat, simpan, preferensi).
Perintah, link, dan elemen menu
Dua pilihan independen. Semua hal lain di sini berlaku untuk keduanya.
- Elemen, per item. Perintah adalah
<button type="button">; tujuan adalah<a href>yang masih terbuka di tab baru, disalin sebagai URL, dan berfungsi tanpa skrip. Dropdown dapat menggabungkannya secara bebas: dua link dan enam perintah, atau satu perintah dan enam link, tidak mengubah apa pun di bawah ini. - Model interaksi, per dropdown. Tombol panah,
Home/End, danEscmenjadikannya menu: tambahkan focusgroup, biarkan menyimpulkanmenuitemdi setiap item, baik tombol maupun link, dan buat terlihat seperti menu (panel berkelanjutan atau pemicu dengan indikator dropdown; lihat "Panel menu atau tombol menu terpisah" di bawah). Daftar singkat link yang digunakan pengguna untukTabadalah navigasi: tanpa focusgroup, tanparole="menu",role="menuitem", atauaria-haspopup="menu"; pertahankan popover,Esc, dan pemulihan fokus, serta lihat panduan menu navigasi.
Konten tidak pernah menentukan peran. Dropdown link "Produk" dapat berupa menu; dropdown perintah "Akun" tidak dapat berupa navigasi.
Gunakan pola menu hanya untuk widget gabungan dengan satu tab stop dan navigasi tombol panah. Gunakan <div focusgroup="menu nomemory">, bukan elemen <menu> HTML. Jangan tambahkan role="menu" atau role="menuitem" dalam markup bersama: focusgroup native menyimpulkan peran tersebut, dan peran eksplisit memblokir inferensi tersebut. Tambahkan hanya di penggantian browser yang tidak didukung di bawah. nomemory membuat pembukaan kembali dimulai pada item pertama. Focusgroup memindahkan fokus; skrip menangani pratinjau, aktivasi, dan pemulihan fokus.
Membuka dan memosisikan menu
Jadikan setiap pemicu sebagai pemanggil perintah dengan commandfor dan command="toggle-popover". Browser membuka dan menutup popover saat diklik, ditekan Enter, dan ditekan Spasi, serta mempertahankan pemicu di dalam grup tutup ringan popover-nya, sehingga mengklik pemicu yang terbuka akan menutupnya. Jangan gunakan popovertarget atau popovertargetaction; pola ini menggunakan commandfor sebagai gantinya.
Tempatkan setiap popover tepat setelah pemicunya. Browser menambahkan hubungan aria-details implisit hanya jika popover bukan elemen seinduk berikutnya, sehingga penempatan ini menghindarinya tanpa atribut tambahan.
<!-- Command invoker: the browser toggles the popover on click, Enter, and Space -->
<button id="file-trigger" class="trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="file-menu" command="toggle-popover">
File
</button>
<!-- Immediately after its trigger, so browsers add no implicit aria-details -->
<div id="file-menu" popover="auto">
<!-- Nested, not on the popover: Chromium otherwise drops focusgroup keyboard behavior -->
<div focusgroup="menu nomemory" aria-labelledby="file-trigger">
<button type="button">New file</button>
<button type="button">Save</button>
<button id="export-trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="export-menu" command="toggle-popover">
Export <span aria-hidden="true">›</span>
</button>
<!-- Submenu: popover="auto" directly after its trigger, inside the parent focusgroup -->
<div id="export-menu" popover="auto">
<div focusgroup="menu nomemory" aria-labelledby="export-trigger">
<button type="button">PDF</button>
<button type="button">Plain Text</button>
</div>
</div>
</div>
</div>
Beri nama setiap menu dengan aria-labelledby yang mengarah ke pemicunya. Pertahankan indikator submenu dekoratif di CSS, atau tambahkan aria-hidden="true". Berikan gaya aktif yang terlihat pada pemicu dengan aria-expanded="true" agar pengguna dapat mengetahui menu mana yang terbuka atau dipratinjau.
Panel menu atau tombol menu terpisah
Pilih satu formulir untuk pemicu tingkat teratas. Peran, model keyboard, dan gaya harus cocok: tombol yang terpisah secara visual menandakan satu perhentian Tab untuk setiap tombol.
- Menubar.
role="menubar"di penampung (tidak pernah di popover), ditata gayanya sebagai satu panel berkelanjutan tanpa celah di antara pemicu. Satu perhentianTab;ArrowLeft/ArrowRightberpindah antar-pemicu; mengarahkan kursor akan membuka menu. Gunakan untuk panel perintah aplikasi. - Tombol menu terpisah. Setiap pemicu adalah perhentian
Tab-nya sendiri dan terlihat seperti tombol mandiri. Tidak adarole="menubar", tidak ada tombol panah di antara pemicu, tidak ada pratinjau saat fokus, tidak ada peralihan saat kursor diarahkan. Gabungkan dalam<nav>jika pengguna melakukan navigasi, jika tidak, tidak ada peran. Gunakan untuk toolbar dan header halaman.
Bagian selanjutnya dari panduan ini menjelaskan formulir panel menu. Untuk tombol terpisah, simpan semuanya di dalam setiap menu dan hapus perilaku tingkat panel menu.
Menempatkan menu dengan CSS Anchor Positioning
Gunakan pemosisian anchor CSS, bukan koordinat JavaScript. command="toggle-popover" dan showPopover({ source: trigger }) menetapkan anchor implisit, jadi jangan tambahkan aturan anchor-name atau position-anchor per menu. Perilaku menu tidak boleh bergantung pada pemosisian anchor.
/* Top-level menus sit below their trigger. The implicit anchor comes from the invoker or source. */
[popover] {
position-area: block-end span-inline-end;
inset: auto;
}
/* A popover nested in another popover is a submenu: beside its parent item, flipping when it would overflow. */
[popover] [popover] {
position-area: inline-end span-block-end;
position-try-fallbacks: flip-inline, flip-block, --below;
}
/* Last resort for narrow viewports: directly below the trigger, indented, so the submenu never
covers its trigger. */
@position-try --below {
position-area: block-end span-inline-end;
margin-inline-start: 1rem;
/* Fit the area: the submenu's minimum width must not stop this fallback from fitting. */
min-inline-size: 0;
max-inline-size: calc(100% - 1rem);
}
Pertahankan --below di posisi terakhir agar hanya diterapkan jika kedua sisi tidak sesuai. Menu ini terbuka tepat di bawah pemicu, sehingga di layar sempit, menu ini menutupi item yang setara di bawah pemicu; ini adalah konsekuensi yang dapat diterima agar menu tetap berada di samping pemicu. Biarkan juga submenu menyusut ke area (min-inline-size: 0, max-inline-size dikurangi indentasi). Penggantian yang tidak sesuai akan dilewati, dan jika tidak ada yang sesuai, browser akan mempertahankan penempatan dasar, sehingga submenu yang terlalu lebar beberapa piksel akan menutupi pemicunya secara otomatis.
Untuk mengetahui cara kerja penggantian balik pada popover biasa, lihat Dropdown yang Memperhitungkan Area Pandang. Panduan tersebut memosisikan grup tombol, bukan menu; untuk menu, penempatan submenu dan penggantian --below di atas lebih diutamakan.
Menganimasikan buka dan tutup
Opsional. Di bawah 150 md, hanya opacity dan translate kecil, sama untuk submenu, di dalam @media (prefers-reduced-motion: no-preference). Fokus berpindah pada toggle, sehingga transisi tidak pernah menunda pengguna keyboard.
@media (prefers-reduced-motion: no-preference) {
/* Closing: this rule is in effect once :popover-open is removed. */
[popover] {
opacity: 0;
translate: 0 -.25rem;
transition: opacity .15s, translate .15s, display .15s allow-discrete, overlay .15s allow-discrete;
}
/* Opening: faster. */
[popover]:popover-open {
opacity: 1;
translate: 0 0;
transition-duration: .1s;
@starting-style { opacity: 0; translate: 0 -.25rem; }
}
/* Switching between open menubar menus is instant, as in native menubars. */
[role="menubar"]:has(> [popover]:popover-open) > [popover] {
transition: none;
}
}
@starting-style bukan pengulangan dari [popover]: popover tertutup adalah display: none, dan transisi tidak berjalan dari status yang tidak dirender, sehingga memberikan nilai sebelum untuk status terbuka. Tanpa itu, buka akan langsung muncul dan hanya tutup yang dianimasikan. display dan overlay dengan allow-discrete membuat popover tetap dirender dan berada di lapisan atas hingga transisi penutupan berakhir.
Keyboard dan fokus
Jangan menambahkan sistem roving-tabindex kedua di dalam focusgroup. Skrip hanya mencakup hal-hal yang tidak dilakukan oleh pemanggil dan grup fokus:
- Satu perhentian tab untuk panel menu.
Tabmasuk di pemicu tingkat teratas pertama.ArrowLeftdanArrowRightberpindah di antara pemicu tingkat teratas dan beralih ke ujung lainnya.HomedanEndpada pemicu akan membuka menunya di item pertama atau terakhir.- Catatan: pemicu yang ditampilkan
Tab. APG memungkinkan pemicu pertama atau terakhir yang digunakan. Panduan ini menentukan yang pertama, tetapi demo memindahkan perhentian tab denganArrowLeftdanArrowRight, sehingga setelah navigasi keyboard,Shift+Tabakan kembali ke pemicu terakhir yang dijangkau. Penggunaan pointer tidak memindahkannya, sehingga urutan yang sama dapat mendarat di pemicu yang berbeda. Kedua perilaku tersebut dapat diterima; pilih salah satu dan pertahankan konsistensinya.
- Catatan: pemicu yang ditampilkan
- Pratinjau. Memfokuskan pemicu tingkat teratas akan menampilkan menunya dan menetapkan
aria-expanded="true". Fokus tetap pada pemicu, dan tidak ada item yang difokuskan. Mengaburkan pemicu akan menutup menu khusus pratinjau. - Enter dan Spasi pada pratinjau. Menutup pratinjau dan tetap fokus pada pemicu. Tindakan
toggle-popovernative menangani hal ini, sehingga skrip tidak boleh mengintersepsi tombol ini. - ArrowDown pada pratinjau. Mengonversinya menjadi menu terbuka dan memfokuskan item pertama.
- Klik pointer pada pemicu yang terbuka atau dipratinjau. Menutupnya secara native; tidak masuk ke menu.
- Perbedaan umum pada pemicu submenu. Klik kursor pada pemicu submenu juga beralih secara native, sehingga dapat menutup submenu yang sudah dipratinjau oleh pengarahan kursor, atau membuka submenu yang belum muncul. Menu native hanya membukanya dan tidak pernah menutupnya. Hal ini tidak berbahaya, sehingga pemandu menyetujuinya.
- Klik pointer pada pemicu tertutup. Membukanya secara native. Fokus tetap pada pemicu; tidak ada item yang difokuskan. Hanya pembukaan keyboard yang masuk ke menu.
- Kursor di atas item. Memfokuskannya tanpa cincin, sehingga pengarahan kursor dan fokus menjadi satu item.
- Penunjuk berada di atas pemicu tingkat atas lainnya saat menu terbuka. Menutupnya dan melihat pratinjau yang diarahkan kursor, memfokuskan pemicunya tanpa cincin. Tidak ada yang terbuka: mengarahkan kursor tidak akan melakukan apa pun.
- Kursor di atas pemicu submenu. Melihat pratinjau submenunya setelah penundaan singkat (fokus kursor menjalankan pratinjau saat fokus yang sama). Item yang setara akan menutupnya.
- Dalam menu.
ArrowUpdanArrowDownberpindah di antara item yang diaktifkan.HomedanEndberpindah ke item pertama dan terakhir yang diaktifkan, yang ditangani oleh skrip. - ArrowLeft atau ArrowRight di menu tingkat atas yang terbuka. Menutupnya dan melihat pratinjau menu yang berdekatan dengan fokus pada pemicunya, yang digabungkan di salah satu ujungnya.
ArrowDownkemudian memasukkannya. Kiri dan Kanan tidak pernah mendarat di dalam menu. - ArrowRight pada pemicu submenu. Membuka submenu dan memfokuskan item pertamanya.
- ArrowRight pada item submenu tanpa submenu turunan. Menutup hierarki dan melihat pratinjau menu tingkat atas berikutnya, sementara fokus tetap pada pemicunya.
ArrowDownkemudian memasukkannya. - ArrowLeft atau Escape di submenu. Hanya menutup submenu tersebut dan mengembalikan fokus ke pemicunya. Menu induk tetap terbuka.
- Enter atau Spasi pada perintah. Mengaktifkannya dan menutup semua menu yang terbuka.
- Tab. Keluar dari sistem menu. Jangan menjebak fokus atau menjalankan setiap perintah satu per satu. Penutupan tidak boleh meninggalkan fokus di dalam konten tersembunyi.
Peristiwa command diaktifkan sebelum toggle-popover membuka menu, sehingga fokus tidak dapat berpindah ke menu di sana. Sebagai gantinya, pindahkan fokus dalam peristiwa toggle. HTML hanya memulihkan fokus untuk popover="auto" pertama dalam stack, sehingga level bertingkat harus memulihkan fokus ke pemicunya secara eksplisit.
Menerapkan interaksi
Terapkan perilaku di atas untuk markup Anda sendiri. Penanganan panah, Home/End, wrap-around, dan Tab untuk keluar adalah kode keydown biasa. Beberapa bagian mudah salah, dikelompokkan di bawah berdasarkan jenis input.
Penanganan keyboard dan fokus
- Satu jalur terbuka. Membuka dengan keyboard akan memanggil
trigger.click(), sehinggatoggle-popoverpemanggil adalah satu-satunya cara untuk membuka menu. Tandai menu agar pengendalitogglememasukkannya, hanya keyboard yang terbuka. Saat pointer terbuka,toggleakan memfokuskan pemicu tanpa cincin, karenapointerdownmencegah fokus dan tombol panah harus tetap berfungsi. - Pratinjau saat fokus. Memfokuskan pemicu akan memanggil
showPopover({ source: trigger })sehingga menu ditambatkan ke pemicu tersebut. Abaikan fokus yang kembali dari dalam menu pemicu itu sendiri, atau dari jendela lain (tanda di jendelablur, dihapus setelah jendelafocus, atau peralihan aplikasi membuka kembali menu terakhir). Sembunyikan menu yang dipratinjau diblurkecuali jika fokus dipindahkan ke menu tersebut. - Pindahkan fokus di
toggle, bukancommand.commanddiaktifkan sebelum menu terbuka. - Pulihkan fokus ke pemicu sendiri. Fokuskan pemicu sebelum
hidePopover(). Setelah aktivasi pointer, pulihkan fokus tanpa cincin (focus({ focusVisible: false })), atau pemicu menampilkan garis batas fokus keyboard setelah klik mouse; aktivasi keyboard mempertahankan cincin. Klik denganevent.detail === 0berasal dari keyboard. - Tutup satu level dalam satu waktu. Panggil
stopPropagation()diEscapedanArrowLeftdi submenu, atau pemroses induk juga akan menutupnya.
Penanganan pointer
- Tombol klik kursor. Panggil
preventDefault()dipointerdownagar klik tidak memindahkan fokus dan memulai pratinjau di tengah klik. Ingat pemicu mana yang ditekan. Mengklik pemicu yang ditutup atau dipratinjau akan membuka atau menutupnya secara native dan tidak akan pernah masuk ke menu; hanya pembukaan keyboard yang akan masuk ke menu. Jangan mengintersepsi klik. - Hover mengikuti fokus. Tata gaya
:hoverdan:focus-visiblesecara terpisah. Padapointermovedi atas item, panggilfocus({ focusVisible: false }); browser yang mengabaikan opsi ini tidak akan menggambar cincin untuk fokus pointer. - Arahkan kursor untuk beralih menu yang terbuka. Pada
pointerenterpemicu tingkat atas saat menu tingkat atas lainnya terbuka: sembunyikan, fokuskan pemicu yang diarahkan kursor tanpa cincin, dan pratinjau saat fokus (lihat di atas) akan menampilkan pratinjaunya. Tidak ada yang terbuka: tidak melakukan apa pun. - Segitiga aman. Jembatan CSS tetap terlalu sempit: jembatan ini hanya berfungsi saat kursor dimulai di dekat tepi induk. Biarkan CSS menggambar jembatan dan gunakan skrip hanya untuk melaporkan pointer:
- Jalankan ini hanya saat
matchMedia('(hover: hover)').matches; tidak relevan dengan sentuhan dan keyboard. - Dimulai saat kursor meninggalkan pemicu submenu yang submenunya terbuka. Lewati submenu
--below, yang tidak memiliki tepi samping. - Tetapkan
--bridge-xdan--bridge-y(pointer, dalam px relatif terhadap submenu),--bridge-edge(tepi dekat x submenu:0saat pointer berada di sebelah kirinya, lebarnya saat dibalik) dan--bridge-heightpada submenu, dandata-bridgeuntuk mengaktifkan jembatan. - Berhenti setelah sekitar 300 md, saat kursor mencapai item di submenu, atau saat submenu ditutup. Hapus pemroses dan atribut. Tidak ada yang berjalan setelah itu.
- CSS memberi submenu
::beforeyang dipotong menjadi segitiga dari pointer ke sudut tepi dekat. Submenu ini berada di atas item seinduk di jalur kursor, sehingga item tersebut tidak pernah menerima pengarahan kursor yang akan menutup submenu.clip-pathjuga membatasi hit testing, sehingga bagian menu induk lainnya tetap interaktif. - Submenu memerlukan
overflow: visible(popover dipotong secara default), dan elemen semu melampauinya, jadi geser titik bentuk sebesar itu. - Tindak lanjut: jalur melengkung (perintah kurva
shape()) mengikuti busur pointer nyata dengan lebih baik daripada segitiga lurus.
- Jalankan ini hanya saat
[popover] [popover] {
overflow: visible;
&[data-bridge]::before {
content: "";
position: absolute;
inset: -100vh -100vw; /* Room for the triangle, which can extend far outside the submenu. */
clip-path: shape(
from calc(100vw + var(--bridge-x)) calc(100vh + var(--bridge-y)),
line to calc(100vw + var(--bridge-edge)) 100vh,
line to calc(100vw + var(--bridge-edge)) calc(100vh + var(--bridge-height)),
close
);
}
}
const canHover = matchMedia('(hover: hover)');
// Call on pointerleave of a submenu trigger, with that trigger's submenu.
function startBridge(submenu, event) {
if (!canHover.matches || !submenu.matches(':popover-open')) return;
const rect = submenu.getBoundingClientRect();
if (event.clientX >= rect.left && event.clientX <= rect.right) return; // --below: no side edge.
const style = submenu.style;
style.setProperty('--bridge-edge', `${event.clientX < rect.left ? 0 : rect.width}px`);
style.setProperty('--bridge-height', `${rect.height}px`);
const update = ({ clientX, clientY, target }) => {
if (target !== submenu && submenu.contains(target)) return stop(); // Reached an item.
style.setProperty('--bridge-x', `${clientX - rect.left}px`);
style.setProperty('--bridge-y', `${clientY - rect.top}px`);
};
const timer = setTimeout(() => stop(), 300);
const stop = () => {
clearTimeout(timer);
document.removeEventListener('pointermove', update);
submenu.removeEventListener('toggle', stop);
submenu.removeAttribute('data-bridge');
};
update(event);
submenu.dataset.bridge = '';
document.addEventListener('pointermove', update);
submenu.addEventListener('toggle', stop, { once: true });
}
Hover follows focus (item 2) tetap menjadi pengendali pointermove biasa: tidak memerlukan logika segitiga.
Selain itu:
- Di luar fokus. Tutup setiap menu yang terbuka saat fokus berada di luar sistem menu (pemroses
documentfocusin). - Aktivasi perintah. Umumkan, lalu sembunyikan menu root dan pulihkan fokus ke pemicunya, tanpa cincin jika aktivasi berasal dari pointer. Untuk item yang dapat dicentang (
menuitemcheckbox,menuitemradio), tunggu sekitar 300 md sebelum menutup agar tanda centang yang diubah terlihat. Gaya pemicu tertutup sebagai aktif hanya pada:focus-visible; jika tidak, fokus yang dipulihkan (tanpa cincin) akan tetap ditandai setelah pemilihan pointer. aria-expanded. Tetapkan hanya di pemrosestoggle. Status ini mencakup status pratinjau dan terbuka, danaria-expandedpemicu tetap"true"saat menunya dipratinjau atau terbuka.- Helper. Penggantian di bawah mengasumsikan helper yang Anda tentukan untuk markup:
triggers(semua tombol[commandfor]),menuFor(trigger),isOpen(menu),closeAndRestore(menu, { fromPointer })(fokuskan pemicu, tanpa cincin saatfromPointer, lalumenu.hidePopover()),enterOnOpen(WeakSetmenu untuk dimasukkan setelah terbuka), danpointerTrigger(pemicu yang terakhir ditekan oleh pointer).
Submenu
- Gunakan
popover="auto"untuk submenu, yang ditempatkan setelah pemicu submenu di dalamfocusgroupinduk. Jangan pernahpopover="manual". - Dengan penyusunan bertingkat, browser dapat membiarkan induk tetap terbuka, menutup submenu yang setara, menutup submenu turunan saat menu root ditutup, dan menangani penutupan ringan saat diklik di luar.
- Pemicu submenu mempertahankan
aria-haspopup="menu"danaria-expanded; pemrosestogglemembuataria-expandedtetap sinkron. - Skrip keyboard menangani
EscapedanArrowLeftbertingkat seperti yang dijelaskan di atas. - Pemicu submenu juga tidak pernah menjalankan perintah: satu kontrol tidak dapat mengaktifkan dan membuka, dan
Entertidak dapat berarti keduanya. Tempatkan tindakan default sebagai item submenu pertama, atau tambahkan item perintah terpisah di samping pemicu. Jika kontrol gabungan dipaksakan, dokumentasikan sebagai konsekuensi aksesibilitas.
Pemisah dan item yang dapat dicentang
- Pisahkan kelompok item dengan
<hr>di dalam focusgroup:separatornative, tidak dapat difokuskan, dilewati oleh focusgroup dan penggantian. Tidak pernah berupa<div>bergaya. - Opsi boolean adalah
<button type="button">denganaria-checkeddanrole="menuitemcheckbox"eksplisit. Focusgroup tidak menyimpulkan peran ini, jadi ini adalah satu-satunya tempat markup bersama menetapkan peran item. - Salah satu dari set adalah
<button type="button">denganrole="menuitemradio"danaria-checked, di dalamrole="group"denganaria-label, dalam grup fokus yang sama. - Tombol skrip
aria-checked. Enterpada kotak centang akan mengaktifkan/menonaktifkan dan menutup menu;Spaceakan mengaktifkan/menonaktifkan dan tetap terbuka (APG). Radio ditutup dengan salah satu tombol.- Render tanda centang dari
[aria-checked="true"]::before, bukan elemen ikon terpisah (lihat CSS di bawah).
Tidak dibahas di sini
- Masukan aktivasi. Menu native akan mem-flash item yang dipilih sebelum ditutup. Jika diinginkan, tampilkan konfirmasi visual ~100 md sebelum
hidePopover(); jangan tunda pemulihan fokus atau pengumuman. - Typeahead. Focusgroup tidak memiliki. Jika diperlukan, pada
keydownyang dapat dicetak, cocokkan label item yang diaktifkan pada tingkat saat ini dan fokuskan kecocokan berikutnya setelah item saat ini, dengan penggabungan. Jangan mengintersepsiSpace.
Verifikasi sebelum menyelesaikan
Petunjuk tidak dapat menjamin perilaku, jadi uji hasilnya. Buka halaman dan periksa setiap elemen ini dengan keyboard dan mouse sungguhan; perbaiki dan uji ulang elemen yang gagal:
- Keyboard.
Tabmendarat di pemicu pertama satu kali;ArrowDownmasuk;Escapemenutup satu level dan mengembalikan fokus ke pemicunya;Tabmenutup setiap menu. - Pointer terbuka. Mengklik pemicu akan membuka menunya dengan fokus pada pemicu dan tanpa cincin; mengklik lagi akan menutupnya.
- Perintah pointer. Mengklik perintah akan menutup setiap menu dan tidak menampilkan cincin fokus pada pemicu.
- Submenu. Berpindah secara diagonal dari tengah pemicu submenu ke item submenu akan membuatnya tetap terbuka; berpindah ke item seinduk yang tidak terkait akan menutupnya.
- Layar sempit. Pada 375 px, tidak ada submenu yang menutupi pemicunya sendiri.
- Tanda centang. Item yang dicentang akan menampilkan tanda centang.
- Tidak ada sisa. Tidak ada error konsol, dan tidak ada
popovertarget,popovertargetaction, atauaria-detailspada pemicu.
Menyelaraskan konten item menu
Jika baris memiliki ikon, label, dan pintasan atau indikator submenu, sediakan slot utama berukuran tetap untuk ikon atau tanda centang dan dorong konten di akhir ke ujung inline. Ikon memiliki ukuran yang diketahui, sehingga slot tetap menjaga label tetap sejajar, baik item memiliki ikon maupun tidak. Jangan gunakan subgrid untuk baris: setiap turunan inline tambahan (misalnya, badge) akan menjadi item petaknya sendiri dan merusak tata letak. Sembunyikan teks pintasan dari teknologi pendukung jika teks tersebut hanya mengulangi tombol yang sudah mengoperasikan item.
[focusgroup~="menu"] {
--menu-icon-size: 1.25rem;
}
/* Target items only. A closed submenu popover inside the focusgroup must keep display: none. */
[focusgroup~="menu"] > button,
[focusgroup~="menu"] > [role="group"] > button {
display: flex;
align-items: center;
gap: .5rem;
}
/* The leading slot holds the icon or check mark. */
[focusgroup~="menu"] button::before {
content: "";
flex: none;
inline-size: var(--menu-icon-size);
}
/* Check mark for checked items. This selector must be at least as specific as the base
::before rule above, or the empty content wins and the check mark never shows. */
[focusgroup~="menu"] button[aria-checked="true"]::before {
content: "✓";
}
/* Shortcut or submenu indicator at the inline end. */
[focusgroup~="menu"] .hint {
margin-inline-start: auto;
}
Tempatkan indikator atau pintasan di akhir <span class="hint" aria-hidden="true"> di dalam tombol; pemicu submenu sudah mengumumkan pop-up-nya.
Progressive enhancement dan penggantian
Penggantian & dukungan browser untuk focusgroup
Pertahankan focusgroup="menu nomemory" di markup. Jika 'focusGroup' in HTMLElement.prototype salah, instal tombol Atas, Bawah, Beranda, dan Akhir, tetapkan role="menu" di setiap penampung menu, dan role="menuitem" di tombol tanpa peran; biarkan item yang dapat dicentang, <hr>, dan role="group". Jangan pernah menginstalnya saat grup fokus native ada. Left, Right, preview, open, dan close adalah skrip penulis.
if (!('focusGroup' in HTMLElement.prototype)) {
document.querySelectorAll('[focusgroup~="menu"]').forEach(group => {
group.setAttribute('role', 'menu');
group.querySelectorAll('button:not([role])').forEach(item => {
item.setAttribute('role', 'menuitem');
});
group.addEventListener('keydown', event => {
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
// Own items only: a nested submenu has its own focusgroup, a role="group" wrapper does not.
const items = [...group.querySelectorAll('button')]
.filter(item => item.closest('[focusgroup]') === group)
.filter(item => !item.disabled && item.getAttribute('aria-disabled') !== 'true');
const current = items.indexOf(document.activeElement);
if (current < 0) return;
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
(current + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
event.preventDefault();
event.stopImmediatePropagation();
items[next].focus({ preventScroll: true });
}, true);
});
}
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.
Penggantian & dukungan browser untuk Pemosisian anchor
Tanpa CSS Anchor Positioning, popover akan kembali ke penempatan area pandang terpusat UA. Jangan pernah mengirimkannya untuk menu: tindakan ini akan melepaskan menu dari pemicunya, dan submenu yang berada di tengah akan menutupi induknya. Batasi aturan yang ditambatkan di balik @supports (position-area: block-end) dan posisikan menu dari skrip jika gagal:
- Tempatkan menu di bawah pemicunya (tingkat atas) atau di sampingnya (submenu), balikkan ke sisi lain dan jepit ke area pandang saat akan meluap.
- Jalankan di pengendali
togglesaat menu terbuka, dan terapkan hasilnya sebagaileftdantoppada menu. Gunakanposition: fixeddenganinset: autodanmargin: 0agar koordinatnya relatif terhadap area pandang. - Pertahankan penggantian ini di dalam
@supports not (position-area: block-end), sehingga browser dengan pemosisian anchor tidak terpengaruh.
Jika menargetkan penerapan sebelumnya yang tidak menetapkan anchor implisit showPopover({ source }) (tidak ada source di ToggleEvent.prototype), berikan anchor-name unik untuk setiap pemicu dan position-anchor yang cocok untuk setiap menu dari skrip, bukan menulis aturan CSS per menu.
Untuk browser yang sama sekali tidak memiliki pemosisian anchor, lihat Dropdown yang Memperhatikan Area Pandang untuk rute polyfill.
Penggantian & dukungan browser untuk perintah Invoker
Tanpa pemanggil, command="toggle-popover" tidak akan melakukan apa pun, sehingga skrip harus mengalihkan menu.
- Deteksi fitur antarmuka, bukan instance:
'commandForElement' in HTMLButtonElement.prototype. - Pada setiap pemicu
click, panggilpreventDefault()agar browser dengan pemanggil tidak dapat beralih dua kali. - Jika menu terbuka, tutup dengan
closeAndRestore(menu). Jika tidak, panggilmenu.showPopover({ source: trigger }). - Tandai menu di
enterOnOpenkecuali jika klik berasal dari pemicu yang ditekan (pointerTrigger), sehingga hanya pembukaan keyboard yang masuk ke menu. EnterdanSpacemasih berfungsi karena menghasilkan klik pada tombol.