Keyboard navigation artinya seluruh halaman bisa dipakai tanpa mouse: Tab dan Shift+Tab berpindah antar elemen, Enter dan Spasi menekan, tombol panah bergerak di dalam satu widget, Escape menutup. Pemakainya pengguna screen reader, orang yang tangannya gak bisa memegang mouse, dan orang yang memang lebih cepat dengan keyboard. Bayangin lorong Homestay Kabut di malam hari. Tab itu berjalan dari pintu ke pintu di lorong. Masuk ke satu kamar, kamu gak keluar ke lorong untuk pindah dari lemari ke laci; kamu bergerak di dalam kamar (tombol panah). Escape itu keluar lagi lewat pintu yang tadi kamu masuki.
🧪 Tebakan cepat: Halaman Kamar Teras punya navigasi (Beranda, Kamar), tiga tab foto (Kamar, Kamar mandi, Teras) yang masing-masing bisa di-Tab, panel fotonya, tiga tombol malam, lalu tombol "Pesan kamar, bayar di tempat". Fokus Pak Hadi ada di link "Kamar". Berapa kali dia harus menekan Tab sampai ke tombol Pesan? Jawabannya ada di bagian "Satu Tab stop untuk satu widget".
Tab, Shift+Tab, dan urutan fokus
Tab memindahkan fokus ke elemen berikutnya yang bisa difokus, sesuai urutan di HTML; Shift+Tab mundur. Enter menekan link dan tombol; Spasi menekan tombol dan mencentang checkbox atau radio. Tiga kriteria WCAG mengaturnya:
- 2.1.1 Keyboard (level A): semua fungsi bisa dipakai dengan keyboard.
- 2.4.3 Focus Order (level A): urutan fokusnya masuk akal dan menjaga makna.
- 2.4.7 Focus Visible (level AA): fokusnya kelihatan, misalnya garis
:focus-visibledari pelajaran sebelumnya.
tabindex mengatur siapa yang ikut urutan Tab:
| Nilai | Artinya |
|---|---|
tabindex="0" |
ikut urutan Tab, di posisinya di HTML |
tabindex="-1" |
gak ikut urutan Tab, tapi bisa difokus lewat kode (elemen.focus()) |
tabindex="1" atau lebih |
melompat ke depan semua elemen lain. Hindari: urutan Tab jadi gak sama dengan urutan halaman |
Satu catatan untuk tes di Mac: Safari, secara bawaan, cuma memindahkan Tab ke kolom isian. Di WebKit 26.4 kami, Tab melewati link, tombol, dan radio. Pakai Option+Tab, atau nyalakan "Press Tab to highlight each item" di pengaturan Advanced Safari.
Satu Tab stop untuk satu widget
Tab foto, grup radio, dan toolbar disebut composite widget: satu widget berisi banyak bagian. ARIA Authoring Practices Guide (APG, "Developing a Keyboard Interface") menyarankan satu widget cukup satu Tab stop; di dalamnya, pindah dengan tombol panah. Caranya roving tabindex: cuma bagian yang aktif yang tabindex="0", sisanya tabindex="-1", dan tombol panah memindahkan tabindex="0" itu bersama fokusnya.
Pola Tabs di APG menulis tombolnya:
| Tombol | Yang terjadi |
|---|---|
| Tab | masuk ke tab yang aktif; Tab sekali lagi ke panelnya |
| Panah kanan | tab berikutnya; dari tab terakhir kembali ke yang pertama |
| Panah kiri | tab sebelumnya; dari tab pertama ke yang terakhir |
| Home, End (opsional) | tab pertama, tab terakhir |
APG menyarankan tab langsung aktif saat difokus, selama panelnya tampil tanpa jeda.
const tabs = [...document.querySelectorAll('[role="tab"]')];
document.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
const i = tabs.indexOf(document.activeElement);
const next = { ArrowRight: (i + 1) % tabs.length, ArrowLeft: (i - 1 + tabs.length) % tabs.length,
Home: 0, End: tabs.length - 1 }[e.key];
if (next === undefined) return; // tombol lain dibiarkan
e.preventDefault(); // panah gak menggulung halaman
tabs.forEach((t, j) => {
t.tabIndex = j === next ? 0 : -1; // roving: cuma satu yang 0
t.setAttribute('aria-selected', String(j === next));
document.getElementById(t.getAttribute('aria-controls')).hidden = j !== next;
});
tabs[next].focus();
});
Fokus di tab Kamar, lalu panah kanan:
| Kamar | Kamar mandi | Teras | |
|---|---|---|---|
| Sebelum | fokus, tabindex="0" |
tabindex="-1" |
tabindex="-1" |
| Sesudah | tabindex="-1" |
fokus, tabindex="0" |
tabindex="-1" |
Kami mengujinya di Chromium 147, WebKit 26.4, dan Firefox 146: panah kanan dari Teras kembali ke Kamar, Home ke Kamar, End ke Teras, dan Tab dari tab yang aktif masuk ke panelnya.
Jadi jawaban tebakannya: 8 kali. Tiga tab, panel, tiga tombol malam, lalu Pesan. Dengan roving tabindex di tab foto dan radio untuk malam (bagian berikutnya), cukup 4 kali: tab aktif, panel, malam yang terpilih, Pesan. Itu jawaban tebakannya.
Radio malam: tombol panah gratis dari browser
Pilihan 1, 2, atau 3 malam sebenarnya grup radio: satu pilihan dari beberapa. <input type="radio"> dengan name yang sama sudah punya roving tabindex sendiri.
<fieldset class="nights">
<legend>Lama menginap</legend>
<label><input type="radio" name="malam" value="1"> 1 malam</label>
<label><input type="radio" name="malam" value="2" checked> 2 malam</label>
<label><input type="radio" name="malam" value="3"> 3 malam</label>
</fieldset>
- Tab masuk ke grup langsung ke radio yang terpilih (2 malam); Tab berikutnya keluar dari grup.
- Panah kanan atau bawah memindahkan fokus sekaligus memilih; panah kiri atau atas mundur.
- APG (pola Radio Group) menulis panah dari radio terakhir kembali ke yang pertama. Chromium dan Firefox melakukannya; WebKit 26.4 berhenti di 3 malam. Jangan menyusun petunjuk yang bergantung pada putaran itu.
Itulah aturan pertama ARIA lagi: elemen asli memberi perilaku keyboard tanpa satu baris JavaScript.
Focus trap hanya di modal
Focus trap adalah fokus yang berputar di dalam satu bagian halaman. Cuma modal yang boleh melakukannya, karena halaman di belakangnya memang sedang gak bisa dipakai. <dialog> yang dibuka dengan showModal() mengerjakannya sendiri. Kami mengujinya lagi di ketiga browser: Enter di tombol Pesan memindahkan fokus ke tombol Batal di modal, Tab gak pernah mendarat di halaman belakang, Escape menutup, dan fokus kembali ke tombol Pesan.
Untuk modal dari <div>, kamu menulis trap-nya sendiri, misalnya fungsi trapFocus:
function trapFocus(modal, closeModal) {
const focusable = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') return closeModal(); // jalan keluar
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
});
}
first dan last adalah elemen pertama dan terakhir yang bisa difokus di dalam modal. Shift+Tab di elemen pertama melompat ke elemen terakhir di modal, dan Tab di elemen terakhir kembali ke yang pertama.
WCAG 2.1.2 (No Keyboard Trap, level A) meminta orang selalu bisa keluar dengan keyboard dari bagian yang dia masuki. Modal lolos karena Escape dan Batal menutupnya. Trap di tempat lain, misalnya pemilih tanggal yang menelan Tab, gagal.
Escape
APG memakai Escape untuk menutup apa pun yang terbuka di atas halaman: dialog, menu, popover. Setelah tertutup, fokus kembali ke tombol yang membukanya, supaya Pak Hadi gak mulai lagi dari atas halaman. <dialog> melakukannya sendiri; untuk popup buatanmu sendiri:
popup.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
popup.hidden = true;
trigger.setAttribute('aria-expanded', 'false');
trigger.focus(); // fokus kembali ke pembukanya
}
});
⚠️ Jebakan yang sering ketemu
tabindexpositif.tabindex="1"di tombol Daftar membuatnya difokus paling awal, lalu Tab melompat kembali ke awal halaman. Pakai0, dan atur urutan lewat urutan HTML.- Semua bagian widget
tabindex="0". Tiga tab jadi tiga Tab stop, dan pengguna keyboard harus melewati semuanya. Roving tabindex: satu0, sisanya-1. - Lupa
preventDefault()di tombol panah. Fokus pindah, tapi halamannya ikut menggulung. - Focus trap di luar modal, atau modal yang gak bisa ditutup dengan Escape. Melanggar WCAG 2.1.2.
- Tes dengan Tab di Safari tanpa Option. Tombol dan link dilewati, dan kamu mengira halamannya rusak.
TL;DR: Semua fungsi harus bisa dipakai dengan keyboard (WCAG 2.1.1), dengan urutan yang masuk akal (2.4.3) dan fokus yang terlihat (2.4.7). tabindex="0" ikut urutan Tab, "-1" cuma lewat kode, positif dihindari karena mengacaukan urutan. Satu widget satu Tab stop: roving tabindex membuat cuma bagian aktif yang tabindex="0"; panah kanan dan kiri berpindah dan berputar, Home dan End ke ujung (pola Tabs APG); setelah panah kanan, tab baru dapat fokus dan tabindex="0", yang lama jadi -1. Radio dengan name yang sama memberi perilaku itu gratis (Tab ke yang terpilih, panah memilih). Focus trap cuma untuk modal: showModal() mengerjakannya; trap buatan sendiri memindahkan Shift+Tab dari elemen pertama ke elemen terakhir. Escape menutup dan mengembalikan fokus ke pembukanya, karena WCAG 2.1.2 meminta selalu ada jalan keluar. Di Safari, tes dengan Option+Tab.