Поиск с автодополнением
Input с выпадающим списком подсказок, фильтрующихся по мере ввода — debounce + ARIA
comboboxдля доступности.
Задача
Поле поиска должно показывать подсказки по мере ввода: фильтрация локального массива или запрос к API с задержкой.
Решение
<div class="autocomplete" id="autocomplete">
<input
class="autocomplete__input"
id="searchInput"
type="text"
placeholder="Начните вводить..."
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-autocomplete="list"
aria-controls="suggestionsList"
autocomplete="off"
/>
<ul
class="autocomplete__list"
id="suggestionsList"
role="listbox"
hidden
></ul>
</div>
.autocomplete { position: relative; max-width: 400px; }
.autocomplete__input {
width: 100%;
padding: 10px 14px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 0.95rem;
}
.autocomplete__input:focus { outline: 2px solid #3b82f6; outline-offset: 1px; }
.autocomplete__list {
position: absolute;
top: calc(100% + 4px);
left: 0; right: 0;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
list-style: none;
padding: 4px 0;
margin: 0;
z-index: 100;
max-height: 240px;
overflow-y: auto;
}
.autocomplete__item {
padding: 9px 14px;
cursor: pointer;
font-size: 0.9rem;
}
.autocomplete__item:hover,
.autocomplete__item[aria-selected="true"] { background: #f1f5f9; }
.autocomplete__item mark {
background: #fef08a;
border-radius: 2px;
padding: 0 1px;
}
const input = document.getElementById('searchInput');
const list = document.getElementById('suggestionsList');
// Локальные данные (или заменить на API-запрос)
const ITEMS = ['JavaScript', 'TypeScript', 'React', 'Vue', 'Angular', 'Node.js', 'CSS', 'HTML'];
function debounce(fn, ms) {
let timer;
return (...args) => { clearTimeout(timer); timer = setTimeout( => fn(...args), ms); };
}
function highlight(text, query) {
const re = new RegExp(`(${query.replace(/[.*+?^${}|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(re, '<mark>$1</mark>');
}
function showSuggestions(query) {
const matches = ITEMS.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
).slice(0, 8);
list.innerHTML = '';
if (!matches.length || !query) {
closeList;
return;
}
matches.forEach((item) => {
const li = document.createElement('li');
li.className = 'autocomplete__item';
li.role = 'option';
li.innerHTML = highlight(item, query);
li.addEventListener('mousedown', (e) => {
e.preventDefault(); // не убирать фокус с input
input.value = item;
closeList;
});
list.appendChild(li);
});
list.hidden = false;
input.setAttribute('aria-expanded', 'true');
}
function closeList() {
list.hidden = true;
input.setAttribute('aria-expanded', 'false');
}
input.addEventListener('input', debounce((e) => showSuggestions(e.target.value), 250));
input.addEventListener('blur', closeList);
document.addEventListener('click', (e) => {
if (!input.contains(e.target)) closeList;
});
Ключевые моменты
debounceнаinput— не искать при каждом символе, а подождать паузу (250 мс для локального фильтра, 400+ для API).e.preventDefault()вmousedownна пункте — без негоblurсрабатывает раньшеclickи список закрывается до выбора.- ARIA
combobox/listbox/option— screen reader объявит список подсказок. markтег с подсветкой — помогает пользователю видеть что совпало с запросом.
Варианты
<datalist>— нативное автодополнение без JS:<input list="items">+<datalist id="items">.- Combobox с API: заменить массив на
fetch('/api/search?q=' + query)вshowSuggestions.