Ввод
Выбираемая карточка
Радиокнопка или чекбокс, у которых есть заголовок и объяснение последствий. Голой подписи в одну строку не хватает, когда у вариантов разная цена.
Разметка
<label class="inst-choice-card">
<input type="radio" name="mode" checked>
<span><span class="inst-choice-card-title">Параллельно</span>
<span class="inst-choice-card-desc">До 12 агентов сразу. Быстрее, но расход токенов выше и порядок вывода не гарантирован.</span></span>
</label>
<label class="inst-choice-card">
<input type="radio" name="mode">
<span><span class="inst-choice-card-title">Последовательно</span>
<span class="inst-choice-card-desc">По одному агенту. Медленнее, зато лог читается сверху вниз без чересполосицы.</span></span>
</label>
Использование
<label class="inst-choice-card">
<input type="radio" name="plan">
<span><span class="inst-choice-card-title">Заголовок</span>
<span class="inst-choice-card-desc">Объяснение в одну-две строки.</span></span>
</label>
| Что | Обязательно | Почему |
|---|---|---|
<label> как контейнер |
да | Нажатие в любое место карточки попадает в контрол. Обёртка <div> превратила бы карточку в мёртвую площадь вокруг маленького кружка |
<input> прямым ребёнком |
да | Состояния написаны через :has(> input…): контрол в подобёртке их не включит |
Один <span> под текст |
да | Сетка карточки двухколоночная: контрол и один текстовый блок. Два соседних узла разъедут колонки |
inst-choice-card-title |
да | Без заголовка это просто чекбокс в рамке |
Общий name у радиогруппы |
да | Без него варианты не исключают друг друга |
fieldset с legend вокруг группы |
да | Иначе вопрос, на который отвечают карточки, нигде не назван |
inst-choice-card-desc |
нет, но обычно да | Без объяснения карточка не оправдывает своей площади — берите переключатели |
Когда использовать
| Используйте | Возьмите другое |
|---|---|
| Выбор режима, у которого есть последствия и их надо объяснить | Вариант, объяснимый одним словом — переключатели: карточка вокруг слова «Да» это лишняя рамка |
| Два-четыре варианта, показанные все сразу | Много вариантов или длинный список — селект: двенадцать карточек это не выбор, а страница |
| Взаимоисключающий выбор одного из режимов | Переключение вида одних и тех же данных — сегментированный контрол: он живёт в панели инструментов и не занимает высоту |
| Набор независимых опций с описаниями — чекбоксы внутри | Включение и выключение одной настройки — свитч в переключателях |
| Карточка выбирается | Карточка открывается или ведёт куда-то — карточка со ссылкой: у выбираемой нет перехода |
Варианты
Внутри — настоящий нативный контрол, и от его типа зависит вся семантика
выбора. Кит не различает эти два случая ничем, кроме того, что рисует
браузер: правило :has(> input:checked) одинаково работает и там, и там.
Разметка
<label class="inst-choice-card">
<input type="checkbox" checked>
<span><span class="inst-choice-card-title">Собирать трассировку</span>
<span class="inst-choice-card-desc">Полный лог каждого шага. Прогон становится примерно на 15% медленнее.</span></span>
</label>
<label class="inst-choice-card">
<input type="checkbox">
<span><span class="inst-choice-card-title">Оповестить по завершении</span>
<span class="inst-choice-card-desc">Письмо на адрес учётной записи, когда очередь опустеет.</span></span>
</label>
| Тип | Когда | Обязательно |
|---|---|---|
type="radio" |
Один из взаимоисключающих режимов | Общий name у всей группы, иначе взаимоисключения не будет |
type="checkbox" |
Независимые опции, любое количество | name у каждого свой |
Группа радиокарточек — это группа полей, и у неё должно быть общее имя:
оберните её в fieldset с legend, иначе скринридер объявит четыре
несвязанных варианта без вопроса, на который они отвечают.
Состояния
Разметка
<label class="inst-choice-card">
<input type="radio" name="st" checked>
<span><span class="inst-choice-card-title">Выбрано</span>
<span class="inst-choice-card-desc">Рамка акцентом, тонированный фон и несущая метка у края.</span></span>
</label>
<label class="inst-choice-card">
<input type="radio" name="st">
<span><span class="inst-choice-card-title">Не выбрано</span>
<span class="inst-choice-card-desc">Обычная рамка контрола.</span></span>
</label>
<label class="inst-choice-card">
<input type="radio" name="st" disabled>
<span><span class="inst-choice-card-title">Недоступно</span>
<span class="inst-choice-card-desc">Недоступно на текущем тарифе.</span></span>
</label>
| Состояние | Как ставится | Что происходит |
|---|---|---|
| наведение | :hover |
Рамка темнеет до --text-muted |
| выбрано | :has(> input:checked) |
Рамка --accent-border, фон --accent-bg и несущая метка у внутреннего края |
| фокус | :has(> input:focus-visible) |
Кольцо вокруг всей карточки, а не вокруг кружка |
| недоступно | :has(> input:disabled) |
Прозрачность 0.5, курсор not-allowed |
Выбор несёт не только цвет. У края выбранной карточки встаёт та же несущая
метка толщиной --size-marker, что у текущего пункта навигации и у активной
вкладки: один язык на весь кит, и он переживает и дальтонизм, и печать.
Композиции
С бейджем в заголовке
Разметка
<label class="inst-choice-card">
<input type="radio" name="tier">
<span><span class="inst-choice-card-title">Распределённо
<span class="inst-badge" data-tone="warn">бета</span></span>
<span class="inst-choice-card-desc">Агенты уезжают на пул воркеров. Пока без гарантий по времени.</span></span>
</label>
Сценарии
Группа с общим вопросом
Разметка
<fieldset class="inst-fieldset">
<legend>Режим прогона</legend>
<div class="inst-grid">
<label class="inst-choice-card">
<input type="radio" name="run-mode" checked>
<span><span class="inst-choice-card-title">Параллельно</span>
<span class="inst-choice-card-desc">До 12 агентов сразу.</span></span>
</label>
<label class="inst-choice-card">
<input type="radio" name="run-mode">
<span><span class="inst-choice-card-title">Последовательно</span>
<span class="inst-choice-card-desc">По одному агенту.</span></span>
</label>
</div>
</fieldset>
Группа полей идёт без рамки по умолчанию: карточки уже обведены, и рамка вокруг них дала бы рамку внутри рамки. См. форму.
Правила
Так Карточка там, где есть последствия
Заголовок называет вариант, описание объясняет, чем за него платят.
Не так Карточка вокруг одного слова
Вариант, объяснимый словом «Да», — это переключатель. Рамка вокруг него не добавляет смысла, только площадь.
Так fieldset с legend вокруг группы
Иначе скринридер объявит четыре несвязанных варианта без вопроса.
Не так Двенадцать карточек
Это уже не выбор, а страница. От пяти вариантов — селект.
Доступность
| Контрол | Нативный <input>, не div с role. Клавиатура, объявление состояния и участие в форме приходят от платформы |
| Клавиатура | Tab — вход в группу, стрелки — переключение между радиокнопками, Space — чекбокс. Ничего не перехватывается |
| Фокус | Кольцо вокруг всей карточки: у контрола внутри своё кольцо было бы вокруг кружка размером с точку |
| Имя | Заголовок и описание внутри <label> целиком становятся доступным именем варианта. Отдельный aria-label не нужен и вредит: он перекрыл бы описание |
| Группа | fieldset с legend — иначе варианты объявляются без вопроса |
| Цвет не единственный носитель | Выбор несёт метку у края и нативную отметку контрола, а не только фон и рамку |
| Цель нажатия | Вся карточка. Это самая большая цель во всём ките |
| Недоступность | disabled на <input> — нативная. Карточка гаснет целиком, и видно, какой именно вариант недоступен |
API
| Имя | Значение | Что делает |
|---|---|---|
| класс | ||
inst-choice-card | — | <label>-контейнер. Сетка, рамка, состояния |
inst-choice-card-title | — | Заголовок варианта |
inst-choice-card-desc | — | Объяснение последствий |
| токен | ||
--pad-card | var(--space-6) | |
--radius-md | 7px | |
--surface-raised | light-dark(var(--n-0), var(--n-12)) | |
--border-control | light-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38)) | |
--accent-border | light-dark(var(--a-4), var(--a-3)) | |
--accent-bg | light-dark(var(--a-1), color-mix(in oklab, var(--a-4) 15%, transparent)) | |
--accent-solid | var(--a-4) | |
--size-marker | 2px | |
--gap-inline | var(--space-4) | |
--text-sm | 0.8125rem | |
--text-xs | 0.75rem | |
--leading-ui | 1.4 | |
--dur-1 | 80ms | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument