Ввод
Селект
Выбор одного значения из списка. Нативный <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> выглядит как окно списка и на телефоне неудобен |
Во всех остальных случаях нативный выигрывает, и заменять его — работа без выигрыша.
Свой список
Поверхность и клавиатуру даёт кит. Приложению остаются данные: открыть, отфильтровать, записать выбор в скрытое поле.
Разметка
<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-field | light-dark(var(--n-1), var(--n-14)) | |
--control-h-sm/md/lg | — | |
--control-pad-sm | var(--space-4) | |
--size-chevron | 10px | |
--space-7 | 24px | |
--radius-sm | 5px | |
--border-control | light-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38)) | |
--text-muted | light-dark(var(--n-8), var(--n-6)) | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument