Combobox

콤보박스

A form control that combines free text input with a filtered list of candidates.

Also known as: AutocompleteTypeaheadSearchable select
···
html
<div class="cb">
  <label class="lbl" for="ci">도시</label>
  <input id="ci" role="combobox" aria-expanded="false" aria-controls="cl" autocomplete="off" placeholder="검색…">
  <ul class="list" id="cl" role="listbox">
    <li role="option" data-v="서울">서울</li>
    <li role="option" data-v="서산">서산</li>
    <li role="option" data-v="서귀포">서귀포</li>
    <li role="option" data-v="인천">인천</li>
  </ul>
</div>
css
.cb{width:min(220px,88%);position:relative}
.lbl{font-size:11px;color:var(--muted);display:block;margin-bottom:5px}
#ci{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--line);background:var(--surface);color:var(--fg);font-size:13px}
.list{position:absolute;left:0;right:0;top:calc(100% + 4px);margin:0;padding:5px;list-style:none;
  background:var(--surface);border:1px solid var(--line);border-radius:9px;box-shadow:0 14px 28px rgba(0,0,0,.18);
  opacity:0;pointer-events:none;transform:translateY(4px);transition:opacity .15s,transform .15s;max-height:120px;overflow:auto}
.list[data-open]{opacity:1;transform:translateY(0)}
.list li{padding:6px 9px;border-radius:6px;font-size:12px;color:var(--fg)}
.list li[hidden]{display:none}
.list li[data-hi]{background:var(--accent);color:#fff}
js
const input = document.getElementById('ci'), list = document.getElementById('cl');
const options = [...list.querySelectorAll('li')];
function filter(q) {
  let first = null;
  options.forEach((o) => {
    const match = !q || o.dataset.v.includes(q);
    o.hidden = !match;
    o.removeAttribute('data-hi');
    if (match && !first) first = o;
  });
  if (first) first.setAttribute('data-hi', '');
  return first;
}
let auto = true;
input.addEventListener('input', () => { auto = false; list.setAttribute('data-open', ''); filter(input.value); });
input.addEventListener('focus', () => { if (!auto) list.setAttribute('data-open', ''); });
document.addEventListener('click', (e) => { if (e.target !== input) list.removeAttribute('data-open'); });

const word = '서울';
function loop() {
  if (!auto) return;
  input.value = ''; list.setAttribute('data-open', ''); filter('');
  let i = 0;
  const type = setInterval(() => {
    if (!auto) return clearInterval(type);
    i++;
    input.value = word.slice(0, i);
    const hit = filter(input.value);
    if (i >= word.length) {
      clearInterval(type);
      setTimeout(() => { if (!auto) return; if (hit) input.value = hit.dataset.v; list.removeAttribute('data-open'); setTimeout(loop, 1300); }, 500);
    }
  }, 420);
}
setTimeout(loop, 500);

The input gets role="combobox" aria-expanded aria-controls; the list gets role="listbox" with role="option" entries. As you type, the currently highlighted candidate is pointed to via aria-activedescendant rather than moving real DOM focus, so the input keeps focus while a screen reader still announces the highlight.

It differs from a dropdown menu in having text input, as described there. It also differs from a native <select> — a select is fine when there are few options to scroll or jump to by first letter, while a combobox is for hundreds or thousands of options that need typing to narrow down instantly.

Show a "no results" state in place of the list when nothing matches, and Esc should close the list without clearing what was typed.