Кастомный select

Выпадающий список с полной стилизацией — через appearance: none и кастомный дропдаун на <div> с ARIA-атрибутами.

Задача

Нативный <select> стилизуется крайне ограниченно. Нужен красивый выпадающий список с иконками, поиском или группировкой, при этом доступный с клавиатуры.

Решение

Способ 1 — стилизация нативного <select> (простой)

<div class="select-wrap">
  <select class="select" name="country">
    <option value="">Выберите страну</option>
    <option value="ru">Россия</option>
    <option value="us">США</option>
    <option value="de">Германия</option>
  </select>
</div>
.select-wrap {
  position: relative;
  display: inline-block;
}

.select-wrap::after {
  content: '▾';
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  color: #64748b;
}

.select {
  appearance: none;
  -webkit-appearance: none;
  padding: 10px 36px 10px 14px;
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  background: #fff;
  font-size: 0.95rem;
  cursor: pointer;
  min-width: 200px;
}

.select:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 1px;
}

Способ 2 — полностью кастомный (с ARIA)

<div class="custom-select" id="cselect" role="combobox" aria-expanded="false" aria-haspopup="listbox" tabindex="0">
  <div class="custom-select__value" id="cselectValue">Выберите...</div>
  <ul class="custom-select__list" role="listbox" id="cselectList" hidden>
    <li class="custom-select__option" role="option" data-value="ru">Россия</li>
    <li class="custom-select__option" role="option" data-value="us">США</li>
    <li class="custom-select__option" role="option" data-value="de">Германия</li>
  </ul>
</div>
<input type="hidden" name="country" id="cselectHidden" />
.custom-select { position: relative; min-width: 200px; user-select: none; }
.custom-select__value {
  padding: 10px 36px 10px 14px;
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  cursor: pointer;
  background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2364748b' stroke-width='1.5' fill='none'/%3E%3C/svg%3E") no-repeat right 12px center;
}
.custom-select__list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0; right: 0;
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  z-index: 100;
  list-style: none;
  padding: 4px 0;
  margin: 0;
}
.custom-select__option { padding: 9px 14px; cursor: pointer; }
.custom-select__option:hover,
.custom-select__option[aria-selected="true"] { background: #f1f5f9; }
const cs     = document.getElementById('cselect');
const value  = document.getElementById('cselectValue');
const list   = document.getElementById('cselectList');
const hidden = document.getElementById('cselectHidden');

cs.addEventListener('click', () => {
  const open = list.hidden;
  list.hidden = !open;
  cs.setAttribute('aria-expanded', String(!open));
});

list.addEventListener('click', (e) => {
  const opt = e.target.closest('[role="option"]');
  if (!opt) return;
  value.textContent = opt.textContent;
  hidden.value = opt.dataset.value;
  list.hidden = true;
  cs.setAttribute('aria-expanded', 'false');
});

document.addEventListener('click', (e) => {
  if (!cs.contains(e.target)) { list.hidden = true; cs.setAttribute('aria-expanded', 'false'); }
});

Ключевые моменты

  • appearance: none убирает стандартный стиль браузера — можно задать свой background со стрелкой.
  • Для простых задач используй стилизацию нативного <select> (способ 1) — меньше кода, лучше доступность.
  • Полностью кастомный требует ARIA (role="combobox/listbox/option", aria-expanded) — без этого screen reader молчит.
  • <input type="hidden"> — отправляет значение кастомного select в форму.

Связанные рецепты / темы