Combobox
Kolom teks yang menampilkan daftar pilihan yang disarankan saat Anda mengetik, dan bisa dioperasikan sepenuhnya dengan keyboard.
Combobox yang Aksesibel (WCAG 2.2)
Ketik untuk memfilter. Gunakan panah atas/bawah untuk berpindah, Enter untuk memilih, Escape untuk menutup.
- Bandung
- Denpasar
- Jakarta
- Makassar
- Medan
- Semarang
- Surabaya
- Yogyakarta
<style>
#city:focus-visible { outline: 2px solid #2f2f2f; outline-offset: 2px; }
[role="option"][aria-selected="true"] { background: #2f2f2f; color: #fff; text-decoration: underline; }
/* Hidden visually, still read by screen readers */
.visually-hidden {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap;
}
</style>
<label for="city">Kota</label>
<p id="city-hint">
Ketik untuk memfilter. Gunakan panah atas/bawah untuk berpindah,
Enter untuk memilih, Escape untuk menutup.
</p>
<input
id="city"
type="text"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="city-listbox"
aria-describedby="city-hint"
autocomplete="off"
/>
<ul id="city-listbox" role="listbox" aria-label="Kota" hidden>
<li id="city-1" role="option" aria-selected="false">Bandung</li>
<li id="city-2" role="option" aria-selected="false">Denpasar</li>
<li id="city-3" role="option" aria-selected="false">Jakarta</li>
<li id="city-4" role="option" aria-selected="false">Makassar</li>
<li id="city-5" role="option" aria-selected="false">Semarang</li>
</ul>
<!-- Polite live region: the number of results, then the choice -->
<div id="city-status" aria-live="polite" class="visually-hidden"></div>
<script>
const input = document.getElementById('city')
const listbox = document.getElementById('city-listbox')
const status = document.getElementById('city-status')
const all = [...listbox.querySelectorAll('[role="option"]')]
let visible = all
let active = -1
function setOpen(open) {
listbox.hidden = !open
input.setAttribute('aria-expanded', String(open))
if (!open) setActive(-1)
}
function setActive(i) {
all.forEach((o) => o.setAttribute('aria-selected', 'false'))
active = i
if (i < 0) return input.removeAttribute('aria-activedescendant')
visible[i].setAttribute('aria-selected', 'true')
input.setAttribute('aria-activedescendant', visible[i].id)
visible[i].scrollIntoView({ block: 'nearest' })
}
function filter() {
const q = input.value.trim().toLowerCase()
visible = all.filter((o) => {
o.hidden = !o.textContent.toLowerCase().includes(q)
return !o.hidden
})
status.textContent = visible.length + ' hasil tersedia'
}
function choose(option) {
input.value = option.textContent
setOpen(false)
status.textContent = option.textContent + ' dipilih'
}
input.addEventListener('input', () => {
filter()
setOpen(true)
setActive(-1)
})
input.addEventListener('keydown', (e) => {
const n = visible.length
if (e.key === 'ArrowDown') {
e.preventDefault()
setOpen(true)
// Alt+ArrowDown only opens the list
if (!e.altKey && n) setActive(active < n - 1 ? active + 1 : 0)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setOpen(true)
if (n) setActive(active > 0 ? active - 1 : n - 1)
} else if (e.key === 'Enter' && !listbox.hidden && active >= 0) {
e.preventDefault()
choose(visible[active])
} else if (e.key === 'Escape') {
if (!listbox.hidden) setOpen(false)
else input.value = ''
}
})
// Keep focus in the input while an option is clicked
listbox.addEventListener('mousedown', (e) => e.preventDefault())
listbox.addEventListener('click', (e) => {
const option = e.target.closest('[role="option"]')
if (option) choose(option)
})
input.addEventListener('blur', () => setOpen(false))
</script>Membuat Combobox yang Aksesibel
- Gunakan elemen select bila cukup; jika butuh pencarian, ikuti pola combobox WAI-ARIA (role combobox, listbox, dan option)
- Sediakan label yang terlihat, jelas, dan deskriptif
- Dukung navigasi keyboard sepenuhnya (panah atas/bawah, Enter, Escape) dan tandai pilihan aktif dengan aria-activedescendant
- Terapkan autocomplete dan filtering yang sesuai, dan umumkan jumlah hasil lewat live region
- Berikan umpan balik saat item dipilih
Combobox yang Tidak Aksesibel (Harap Dihindari)
Contoh tidak aksesibel. Jangan ditiru.
▼
<div class="dropdown">
<div class="field" style="display: flex; border: 1px solid #ccc">
<!-- No label, only a placeholder; the focus outline is removed -->
<input type="text" placeholder="Pilih kota" style="outline: none" onclick="openList()" />
<!-- A div "button": not focusable, no name, no role, faint colour -->
<div class="arrow" style="color: #999; cursor: pointer" onclick="toggleList()">▼</div>
</div>
<!-- Options are plain divs: no role, no state, mouse only -->
<div class="list" style="display: none">
<div class="item" onclick="pick(this)">Bandung</div>
<div class="item" onclick="pick(this)">Denpasar</div>
<div class="item" onclick="pick(this)">Jakarta</div>
<div class="item" onclick="pick(this)">Makassar</div>
<div class="item" onclick="pick(this)">Semarang</div>
</div>
</div>
<script>
const list = document.querySelector('.dropdown .list')
const field = document.querySelector('.dropdown input')
function openList() {
list.style.display = 'block'
}
function toggleList() {
list.style.display = list.style.display === 'none' ? 'block' : 'none'
}
function pick(item) {
// Nothing is announced and there's no keyboard path to get here
field.value = item.textContent
list.style.display = 'none'
}
</script>Contoh Implementasi Combobox yang Tidak Aksesibel
- Tidak ada peran (role) dan atribut ARIA, sehingga pembaca layar tidak tahu ada daftar pilihan
- Tidak ada dukungan keyboard; daftar hanya bisa dibuka dan dipilih dengan mouse
- Manajemen fokus yang buruk dan indikator fokus dihilangkan (outline: none)
- Tidak ada pengumuman status, misalnya jumlah hasil atau item yang dipilih
- Tidak ada label; hanya placeholder yang hilang saat pengguna mulai mengetik
- Struktur HTML yang tidak tepat: tombol panah dan pilihan dibuat dari div yang hanya punya onClick