Перейти к содержимому
Плотность
Тема

Ввод

Селект

JS по желанию Свой список вместо нативного. Нативный работает без скрипта

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

Компонент
Разметка
<span class="inst-select-wrap">
  <select class="inst-select" aria-label="Модель">
    <option>opus</option>
    <option>sonnet</option>
    <option>haiku</option>
  </select>
</span>

Использование

Обёртка обязательна — в ней живёт шеврон: псевдоэлемент самого <select> браузер не отдаёт.

<span class="inst-select-wrap">
  <select class="inst-select" aria-label="Модель">…</select>
</span>
Что Обязательно Почему
inst-select-wrap на обёртке да Шеврон рисуется псевдоэлементом обёртки. Без неё поле останется без стрелки и будет неотличимо от текстового
inst-select на поле да Врез, рамка, высота и состояния приходят оттуда
<label for> или aria-label да Первый <option> подписью не является

Когда использовать

Используйте Возьмите другое
Больше пяти взаимоисключающих вариантов 2–4 варианта, все на видусегментированный контрол
Список известен заранее и не меняется Список, который нужно искать или дополнять — комбобокс. В ките его пока нет
Значение выбирается редко Независимые флагичекбоксы: в селекте выбран всегда ровно один
Множественный выбор — multiple Действия, а не значенияменю в поповере

Размеры

Пример
Разметка
<span class="inst-select-wrap">
  <select class="inst-select inst-select--sm" aria-label="sm"><option>sm</option></select>
</span>
<span class="inst-select-wrap">
  <select class="inst-select" aria-label="md"><option>md</option></select>
</span>
<span class="inst-select-wrap">
  <select class="inst-select inst-select--lg" aria-label="lg"><option>lg</option></select>
</span>

Симметричны кнопочным и полевым: селект, поле и кнопка одного размера стоят в одной строке формы и разойтись не могут.

Состояния

Пример
Разметка
<span class="inst-select-wrap">
  <select class="inst-select" aria-label="Обычный"><option>Обычный</option></select>
</span>
<span class="inst-select-wrap">
  <select class="inst-select" aria-label="Недоступен" disabled><option>Недоступен</option></select>
</span>
<span class="inst-select-wrap">
  <select class="inst-select" aria-label="Ошибка" aria-invalid="true"><option>Ошибка</option></select>
</span>

Врез, рамка, фокус, ошибка, disabled и readonly — те же, что у текстового поля: все три контрола объявлены одним правилом, поэтому разойтись не могут.

JS

Нативный <select> не требует ни строки: клавиатура, набор по первым буквам, список на весь экран телефона — всё от платформы. Скрипт нужен только там, где нативного мало: поиск по списку, две строки в пункте, иконка ряда.

Когда стоит уходить от нативного

Больше двух-трёх десятков вариантов Нужен поиск, а в <select> его нет
В пункте не только текст Нативный список рисует одну строку без разметки
Выбор нескольких <select multiple> выглядит как окно списка и на телефоне неудобен

Во всех остальных случаях нативный выигрывает, и заменять его — работа без выигрыша.

Свой список

Поверхность и клавиатуру даёт кит. Приложению остаются данные: открыть, отфильтровать, записать выбор в скрытое поле.

Пример
worldgen-01
biomes-04
rivers-12
Разметка
<div class="inst-field" style="max-inline-size:16rem">
  <label class="inst-label" for="pick">Прогон</label>
  <span class="inst-select-wrap">
    <button class="inst-select" type="button" id="pick"
            popovertarget="pick-list" aria-haspopup="listbox">worldgen-01</button>
  </span>
  <div class="inst-popover inst-popover--fill inst-popover--anchored" id="pick-list" popover>
    <input class="inst-input inst-input--sm" type="search" aria-label="Поиск по прогонам" placeholder="Поиск">
    <div role="listbox" aria-label="Прогоны">
      <div class="inst-menu-item" role="option" aria-selected="true" tabindex="0">worldgen-01</div>
      <div class="inst-menu-item" role="option" aria-selected="false" tabindex="-1">biomes-04</div>
      <div class="inst-menu-item" role="option" aria-selected="false" tabindex="-1">rivers-12</div>
    </div>
  </div>
</div>

Стрелки, Home, End и перенос выбора внутри списка выполняет kit.js — это role="listbox". Открытие и закрытие берёт Popover API.

События

list.addEventListener('inst:select', (e) => {
  trigger.textContent = e.detail.value;
  hidden.value = e.detail.value;      // значение уходит с формой
  list.hidePopover();
});

Скрытое поле обязательно: свой список — это разметка, а форма отправляет поля. Без него выбор виден человеку и не виден серверу.

Правила

Так Селект от шести вариантов

Ниже шести выбор быстрее делается сегментированным контролом: все варианты видны, и переключение стоит одного нажатия.

Не так Селект вместо действий

Список действий над объектом — меню в поповере. Селект хранит значение, а не запускает работу.

Так Нативный select

Клавиатура, поиск по первым буквам и системное колесо на телефоне приходят от платформы и не пишутся заново.

Не так Самодельный выпадающий список

Он теряет всё перечисленное и заводит JS ради того, что уже работает.

Доступность

Клавиатура Стрелки, Home/End, поиск по первым буквам, Enter. Всё нативное
Подпись <label for> или aria-label. Первый <option> подписью не является
Шеврон pointer-events: none — клик по стрелке открывает список, а не проваливается мимо
Режим принудительных цветов Шеврон — маска, в forced-colors теряющая цвет фона, поэтому переводится на CanvasText явно
Мобильные Нативный список превращается в системное колесо. Самодельный выпадающий список это теряет

API

ИмяЗначениеЧто делает
класс
inst-select-wrapОбёртка. Обязательна — в ней шеврон
inst-selectСам <select>
модификатор
inst-select--smРазмер
inst-select--lgРазмер
токен
--surface-fieldlight-dark(var(--n-1), var(--n-14))
--control-h-sm/md/lg
--control-pad-smvar(--space-4)
--size-chevron10px
--space-724px
--radius-sm5px
--border-controllight-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38))
--text-mutedlight-dark(var(--n-8), var(--n-6))

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument