콤보박스

Combobox

텍스트를 입력하면 후보 목록이 걸러지는, 입력과 선택이 합쳐진 폼 컨트롤.

다른 이름: 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);

입력창에 role="combobox" aria-expanded aria-controls를, 목록에는 role="listbox"와 각 항목에 role="option"을 씁니다. 입력하는 동안 현재 포커스된 후보는 실제 DOM 포커스를 옮기지 않고 aria-activedescendant로만 가리켜서(입력 포커스를 유지한 채) 스크린리더에 알립니다.

드롭다운 메뉴와 다른 점은 위에서 설명한 대로 텍스트 입력의 유무입니다. 네이티브 <select>와도 다릅니다 — select는 전체 옵션을 스크롤하거나 첫 글자를 눌러 점프하는 식이라 옵션이 적을 때 적합하고, 콤보박스는 옵션이 수백~수천 개라 타이핑으로 즉시 좁혀야 할 때 씁니다.

후보가 없으면 "결과 없음" 상태를 목록 자리에 보여줘야 하고, Esc는 목록만 닫아야지 입력값까지 지우면 안 됩니다.