Sebagian besar pengguna halaman web mengandalkan tetikus, tetapi kelompok besar lain menavigasi seluruh halaman dengan papan ketik. Topik ini membahas urutan fokus yang mengikuti urutan DOM, elemen yang dapat difokuskan secara bawaan, atribut tabindex beserta bahaya nilai positifnya, penanda fokus dengan :focus-visible, dan skip link untuk melewati navigasi yang berulang. Setelah mengikuti topik ini, halaman dapat diperiksa dan diperbaiki agar seluruh fiturnya dapat dicapai tanpa tetikus.
Pengguna papan ketik dan urutan fokus#
Fokus (focus) adalah keadaan satu elemen yang sedang menerima masukan papan ketik, dan elemen yang menerima fokus ditandai garis tepi yang digambar browser. Pengguna papan ketik berpindah antarelemen dengan Tab dan kembali dengan Shift+Tab, sedangkan pembaca layar membacakan elemen yang berfokus. Kelompok pengguna yang mengandalkan papan ketik tidak kecil, mulai dari pengguna dengan gangguan motorik yang sulit mengoperasikan tetikus, pengguna pembaca layar, hingga pengguna mahir yang memilih papan ketik karena lebih cepat.
Urutan fokus mengikuti urutan elemen pada DOM, yaitu urutan penulisan markup, dan bukan posisi visualnya. Perbedaan ini menjadi sumber masalah apabila CSS memindahkan tampilan elemen jauh dari posisi DOM-nya. Sebuah tautan yang tampil di pojok kanan atas karena diatur position atau urutan grid tetap menerima fokus menurut urutan DOM-nya, sehingga garis tepi melompat ke tempat yang tak terduga. Cara paling aman adalah menjaga urutan visual dan urutan DOM tetap searah.
Elemen yang dapat difokuskan secara bawaan#
Browser memberikan fokus secara bawaan kepada elemen yang memang dirancang untuk berinteraksi, dan daftarnya pendek. Berikut elemen yang dapat difokuskan tanpa atribut apa pun.
ayang beratributhref, sedangkanatanpahreftidak dapat difokuskan.button, baik di dalam formulir maupun di luarnya.input, kecualitype="hidden", besertaselectdantextarea.summarypada elemendetails, daniframeyang menyematkan halaman lain.
Elemen yang beratribut disabled keluar dari urutan fokus karena tidak menerima masukan apa pun. Sisanya, seperti div, span, h1, dan p, tidak dapat difokuskan secara bawaan, dan di situlah atribut tabindex berperan dengan tiga nilai yang maknanya berbeda. Daftar di atas menjadi patokan saat mengaudit halaman, karena elemen di luar daftar tidak akan pernah tercapai dengan Tab sebelum diberi tabindex.
tabindex 0, -1, dan bahaya nilai positif#
Atribut global tabindex mengatur hubungan sebuah elemen dengan urutan fokus. Nilai tabindex="0" memasukkan elemen ke urutan tab pada posisinya di DOM, sehingga berguna untuk elemen yang perlu dapat difokuskan tanpa menjadi kontrol interaktif, misalnya panel yang dapat digulir. Nilai tabindex="-1" mengeluarkan elemen dari urutan Tab, tetapi elemen itu tetap dapat menerima fokus dari skrip melalui focus(), dan pola ini digunakan untuk sasaran skip link.
Nilai positif sebaiknya tidak pernah digunakan. Elemen ber-tabindex="1" ke atas mendahului seluruh elemen lain, kemudian diikuti elemen sisanya, sehingga urutan fokus tidak lagi mengikuti urutan DOM. Urutan semacam itu mudah rusak setiap kali markup disunting, karena penulis harus mengingat nomor tabindex pada seluruh halaman.
Memeriksa urutan fokus dengan simulasi#
Skrip berikut meniru cara browser menyusun urutan tab dari satu potongan halaman dengan memindai elemen yang dapat difokuskan secara bawaan dan elemen beratribut tabindex.
// meniru urutan tab dari satu potongan halaman
const potongan = `
<a href="#utama" class="skip">Langsung ke konten</a>
<nav>
<a href="/jadwal">Jadwal</a>
<a href="/arsip">Arsip</a>
</nav>
<input type="text" placeholder="Cari catatan" tabindex="2">
<div class="panel" tabindex="0">Panel catatan</div>
<button type="button">Simpan</button>
<div class="rincian" tabindex="-1">Rincian</div>
<a href="/tentang">Tentang</a>
`;
const TAG = /<([a-z][a-z0-9]*)\b([^>]*)>/g;
const BAWAAN_FOKUS = new Set(["a", "button", "input", "select", "textarea", "summary", "iframe"]);
const masuk = [];
let c;
while ((c = TAG.exec(potongan)) !== null) {
const tag = c[1];
const a = c[2];
const tbM = a.match(/tabindex\s*=\s*"(-?\d+)"/);
const tb = tbM ? Number(tbM[1]) : null;
let fokus = BAWAAN_FOKUS.has(tag);
if (tag === "a" && !/\bhref\s*=/.test(a)) fokus = false;
if (tag === "input" && /type\s*=\s*"hidden"/.test(a)) fokus = false;
if (tb === -1) continue;
if (!fokus && tb === null) continue;
const href = (a.match(/href\s*=\s*"([^"]*)"/) || [])[1];
const jenis = tag === "a" && href ? `a href="${href}"` : tag;
masuk.push({ tb, jenis });
}
const positif = masuk.filter(b => b.tb !== null && b.tb > 0).sort((x, y) => x.tb - y.tb);
const natural = masuk.filter(b => b.tb === null || b.tb === 0);
let nomor = 0;
for (const b of [...positif, ...natural]) {
nomor += 1;
const tanda = b.tb !== null ? `, tabindex="${b.tb}"` : "";
console.log(`${nomor}. ${b.jenis}${tanda}`);
}1. input, tabindex="2"
2. a href="#utama"
3. a href="/jadwal"
4. a href="/arsip"
5. div, tabindex="0"
6. button
7. a href="/tentang"Masukan pencarian melompat ke posisi pertama karena tabindex="2", sedangkan div ber-tabindex="-1" tidak muncul sama sekali. Potongan di atas sengaja memuat satu elemen ber-tabindex positif, satu ber-tabindex="0", dan satu ber-tabindex="-1" agar akibat ketiga nilai itu terlihat sekaligus. Simulasi ini menyederhanakan perilaku browser, yang juga memperhitungkan atribut disabled, contenteditable, dan elemen di dalam iframe, tetapi urutan yang dihasilkan sudah memperlihatkan efek setiap nilai tabindex. Perbaikan yang tepat untuk potongan di atas adalah menghapus tabindex="2" dari masukan pencarian, lalu memindahkan penulisannya ke posisi DOM yang diinginkan. Pemeriksaan yang lebih nyata tetap dilakukan langsung di browser, yaitu menekan Tab dari awal halaman dan mencatat urutan garis fokusnya.
Penanda fokus dan :focus-visible#
Garis tepi fokus adalah satu-satunya petunjuk posisi bagi pengguna papan ketik, sehingga menghapusnya dengan outline: none membuat halaman nyaris tidak dapat dinavigasi. Apabila garis bawaan browser dirasa kurang cocok dengan desain, penandanya ditata ulang dan bukan dihapus. Pemilih :focus-visible menata penanda fokus pada fokus yang muncul dari papan ketik, dan browser menentukan sendiri kapan pemilih itu aktif, sehingga tampilan bagi pengguna tetikus umumnya tetap bersih.
/* menghapus outline mentah-mentah merugikan pengguna papan ketik */
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}Aturan di atas menebalkan dan mewarnai penanda fokus menjadi kuning dengan jarak kecil dari elemennya, sehingga lebih mudah dilihat daripada garis bawaan. Nilai outline-offset memberi ruang antara garis dan tepi elemen, dan penataan ini berlaku untuk setiap elemen yang menerima fokus tanpa perlu menuliskannya satu per satu. Pemilih :focus biasa menandai fokus dari tetikus maupun papan ketik sekaligus, sehingga :focus-visible memberikan pemisahan yang lebih halus antara keduanya.
Skip link#
Skip link adalah tautan pertama pada dokumen yang sasarannya konten utama, sehingga pengguna papan ketik dapat melompati navigasi yang berulang di setiap halaman. Kebutuhannya muncul karena bilah navigasi dengan banyak tautan harus dilalui Tab satu per satu pada setiap halaman baru sebelum isi utamanya tercapai. Skip link biasanya disembunyikan dari tampilan dan baru muncul saat menerima fokus, dan sasarannya adalah elemen main ber-id yang diberi tabindex="-1" agar fokus benar-benar berpindah.
Demo berikut menyimulasikan halaman dengan navigasi tiga tautan. Klik di dalam area demo, lalu tekan Tab untuk melihat skip link muncul dan menerima fokus lebih dahulu, kemudian aktifkan skip link tersebut untuk melompat ke konten utama.
Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Demo skip link</title>
<style>
body { font-family: sans-serif; margin: 0; padding: 0 1rem; }
.skip-link {
position: absolute;
top: -48px;
left: 8px;
background: #1a237e;
color: #ffffff;
padding: 8px 16px;
border-radius: 0 0 4px 4px;
font-weight: 600;
}
.skip-link:focus { top: 0; }
a:focus { outline: 3px solid #f59e0b; outline-offset: 2px; }
nav { background: #e8eaf6; padding: 8px; display: flex; gap: 12px; }
main { max-width: 40rem; }
.petunjuk { color: #555555; }
</style>
</head>
<body>
<!-- tautan pertama pada dokumen: skip link -->
<a class="skip-link" href="#konten-utama">Langsung ke konten utama</a>
<nav aria-label="Navigasi demo">
<a href="/jadwal">Jadwal</a>
<a href="/arsip">Arsip</a>
<a href="/bantuan">Bantuan</a>
</nav>
<!-- sasaran skip link: tabindex="-1" agar fokus benar-benar berpindah -->
<main id="konten-utama" tabindex="-1">
<h1>Catatan piket kelas</h1>
<p class="petunjuk">Klik di dalam area demo, lalu tekan Tab. Skip link muncul di bagian atas dan menerima fokus lebih dahulu. Aktifkan skip link tersebut untuk melompat melewati tiga tautan navigasi.</p>
<p>Isi utama halaman berada di sini, setelah tiga tautan navigasi yang tidak perlu dilalui ulang setiap kali halaman baru dibuka.</p>
</main>
</body>
</html>Pada demo itu, skip link ditata dengan position: absolute dan nilai top negatif sehingga berada di luar layar, lalu top diubah saat :focus. Pola ini digunakan secara luas karena tautannya tetap berada di DOM dan tetap dapat difokuskan, sehingga tidak tersembunyi dari pembaca layar maupun dari urutan Tab.
Berkas utuh fokus.html#
Berkas berikut menyatukan seluruh bagian topik ini pada satu halaman, yaitu skip link sebagai tautan pertama, main ber-id dan tabindex="-1" sebagai sasarannya, panel yang dapat digulir dengan tabindex="0", dan tombol yang memindahkan fokus ke paragraf ber-tabindex="-1".
Buka berkas ini di browser, lalu telusuri seluruhnya hanya dengan Tab. Urutan fokusnya adalah skip link, dua tautan navigasi, panel catatan, dan tombol Tampilkan status piket, kemudian fokus berhenti pada tombol tersebut karena paragraf status tidak menerima fokus dari Tab. Panel catatan ber-tabindex="0" dapat digulir dengan tombol panah setelah menerima fokus, dan itulah gunanya memasukkan panel semacam itu ke urutan tab. Setelah tombol diaktifkan dengan Enter, paragraf status menerima fokus dari skrip dan terbaca oleh pembaca layar. Skrip pada berkasnya hanya berisi satu pendengar event, karena seluruh urutan fokus lainnya dikerjakan oleh urutan markup itu sendiri.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Urutan fokus dan tabindex</title>
<style>
body { font-family: sans-serif; margin: 0 auto; max-width: 36rem; padding: 0 1rem; }
a:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
nav { display: flex; gap: 12px; background: #e8eaf6; padding: 8px; }
.skip-link { position: absolute; top: -48px; left: 8px; background: #1a237e; color: #ffffff; padding: 8px 16px; }
.skip-link:focus { top: 8px; }
.panel { border: 1px solid #bbbbbb; border-radius: 4px; padding: 0.5rem 0.75rem; max-height: 6rem; overflow: auto; }
.rincian { color: #555555; }
</style>
</head>
<body>
<!-- tautan pertama pada dokumen: skip link menuju konten utama -->
<a class="skip-link" href="#konten-utama">Langsung ke konten utama</a>
<nav aria-label="Navigasi utama">
<a href="/jadwal">Jadwal</a>
<a href="/arsip">Arsip</a>
</nav>
<!-- sasaran skip link: tabindex="-1" agar fokus berpindah -->
<main id="konten-utama" tabindex="-1">
<h1>Catatan piket kelas</h1>
<!-- tabindex="0": panel yang dapat digulir ikut masuk urutan tab -->
<div class="panel" tabindex="0">
<p>Panel catatan panjang yang isinya dapat digulir dengan papan ketik setelah panel menerima fokus.</p>
<p>Baris kedua memastikan panelnya benar-benar dapat digulir pada jendela yang sempit.</p>
</div>
<button type="button" id="tampilkan">Tampilkan status piket</button>
<!-- tabindex="-1": sasaran fokus skrip, tidak dilewati Tab -->
<p class="rincian" id="status-piket" tabindex="-1" hidden>
Status piket: jadwal Senin diganti menjadi Ali.
</p>
</main>
<script>
const tombol = document.getElementById("tampilkan");
const status = document.getElementById("status-piket");
tombol.addEventListener("click", () => {
status.hidden = false;
status.focus();
});
</script>
</body>
</html>Kesalahan umum#
Menghapus garis fokus tanpa penggantinya. Aturan outline: none pada semua elemen membuat pengguna papan ketik kehilangan petunjuk posisinya. Tata ulang penandanya dengan :focus-visible, atau pastikan ada indikator lain yang setara.
Menabur tabindex positif untuk menata ulang fokus. Urutan yang dibangun dengan nomor 1, 2, 3 langsung rusak ketika satu elemen ditambahkan atau dihapus. Perbaiki urutan DOM-nya, dan gunakan hanya tabindex="0" atau tabindex="-1".
Menempatkan skip link bukan sebagai tautan pertama. Manfaat skip link hilang apabila pengguna harus melewati tautan lain terlebih dahulu untuk mencapainya. Letakkan skip link sebagai elemen yang dapat difokuskan pertama pada dokumen.
Ringkasan#
- Urutan fokus mengikuti urutan DOM, sehingga tata letak visual dan urutan markup perlu dijaga tetap searah.
tabindex="0"memasukkan elemen ke urutan tab,tabindex="-1"mengeluarkannya dari Tab tetapi tetap menerima fokus dari skrip, dan nilai positif sebaiknya dihindari.:focus-visiblemenata penanda fokus bagi pengguna papan ketik tanpa mengganggu tampilan pengguna tetikus.- Skip link sebagai tautan pertama yang menuju
mainber-iddantabindex="-1"memotong navigasi yang berulang.
Referensi#
- The tabindex attribute — WHATWG — Ketentuan resmi atribut tabindex beserta urutan fokusnya.
- Global attributes: tabindex — MDN Web Docs — Dokumentasi nilai tabindex dan efeknya pada urutan fokus.
- :focus-visible — MDN Web Docs — Dokumentasi pemilih :focus-visible dan perilakunya.
- Keyboard Compatibility — W3C WAI — Ringkasan kebutuhan navigasi papan ketik beserta videonya.