Перейти к содержимому
EN
Масштаб
Плотность
Тема
Акцент

Ввод

Выбираемая карточка

Радиокнопка или чекбокс, у которых есть заголовок и объяснение последствий. Голой подписи в одну строку не хватает, когда у вариантов разная цена.

Компонент
Разметка
<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> как контейнер да Нажатие в любое место карточки попадает в контрол. Обёртка <div> превратила бы карточку в мёртвую площадь вокруг маленького кружка
<input> прямым ребёнком да Состояния написаны через :has(> input…): контрол в подобёртке их не включит
Один <span> под текст да Сетка карточки двухколоночная: контрол и один текстовый блок. Два соседних узла разъедут колонки
inst-choice-card-title да Без заголовка это просто чекбокс в рамке
Общий name у радиогруппы да Без него варианты не исключают друг друга
fieldset с legend вокруг группы да Иначе вопрос, на который отвечают карточки, нигде не назван
inst-choice-card-desc нет, но обычно да Без объяснения карточка не оправдывает своей площади — берите переключатели

Внутри карточки нативный <input>, а не div с ролью: клавиатура, объявление состояния и участие в форме приходят от платформы. Tab входит в группу, стрелки переключают радиокнопки, Space жмёт чекбокс — ничего не перехватывается.

Кольцо фокуса рисуется вокруг всей карточки: у контрола внутри оно было бы вокруг кружка размером с точку. Заголовок и описание внутри <label> целиком становятся именем варианта, поэтому отдельный aria-label не нужен и вредит — он перекрыл бы описание. Группе имя даёт fieldset с legend, иначе варианты объявляются без вопроса.

Выбор несёт метку у края и нативную отметку контрола, а не только фон и рамку. Цель нажатия — вся карточка, самая большая во всей библиотеке. disabled на <input> гасит карточку целиком, и видно, какой именно вариант недоступен.

Варианты

Внутри — настоящий нативный контрол, и от его типа зависит вся семантика выбора. Библиотека не различает эти два случая ничем, кроме того, что рисует браузер: правило :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>

Группа полей идёт без рамки по умолчанию: карточки уже обведены, и рамка вокруг них дала бы рамку внутри рамки. См. форму.

API

Классы
ИмяЧто делает
класс
inst-choice-card<label>-контейнер. Сетка, рамка, состояния
inst-choice-card-titleЗаголовок варианта
inst-choice-card-descОбъяснение последствий
Токены
ИмяЗначениеОткуда
токен
--pad-cardvar(--space-6)база +14: плотность · масштаб
--radius-md8pxбаза +4: масштаб
--surface-raisedlight-dark(var(--n-0), var(--n-12))база +1: тема
--border-controllight-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38))одно на все ячейки
--accent-borderlight-dark(var(--a-4), var(--a-3))одно на все ячейки
--accent-bglight-dark(var(--a-1), color-mix(in oklab, var(--a-4) 15%, transparent))одно на все ячейки
--accent-solidvar(--a-4)одно на все ячейки
--size-marker2pxбаза +4: масштаб
--gap-inlinevar(--space-4)база +14: плотность · масштаб
--text-sm0.875remбаза +4: масштаб
--text-xs0.78125remбаза +4: масштаб
--leading-ui1.4одно на все ячейки
--dur-180msодно на все ячейки

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