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

Навигация

Вкладки

JS Переключение панели. Стрелки и бегущий tabindex делает кит

Горизонтальный переключатель видов внутри одного раздела. По умолчанию — на ссылках: переключение экрана это роутинг, и JS ему не нужен.

Компонент
Разметка
<nav class="inst-tabs" aria-label="Разделы прогонов">
  <a class="inst-tab" href="#" aria-current="page">Активные</a>
  <a class="inst-tab" href="#">История</a>
  <a class="inst-tab" href="#">Расписание</a>
</nav>

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

<nav class="inst-tabs" aria-label="Разделы прогонов">
  <a class="inst-tab" href="/runs/active" aria-current="page">Активные</a>
  <a class="inst-tab" href="/runs/history">История</a>
</nav>
Что Обязательно Почему
<nav> + aria-label на роутинговом варианте да Навигаций на экране несколько, и без имени они неразличимы
aria-current="page" да Активный вид при переключении адресом
role="tablist" + role="tab" + aria-selected + aria-controls да, только у настоящих вкладок Роль без стрелочной навигации и панели — обещание, которого никто не выполнит
type="button" у вкладки-кнопки да Иначе внутри формы она её отправит

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

Используйте Возьмите другое
Несколько видов одних и тех же данных, у каждого свой адрес Постоянный список разделов приложениянавигация в боковой колонке
2–7 видов, названия которых помещаются в строку Выбор одного значения из равныхсегментированный контрол: это ввод, а не переход
Настоящие вкладки внутри страницы — с role="tablist", см. ниже Длинный списокселект: вкладки не сворачиваются в список
Путь до текущего экрана — рядом Иерархия, а не братьякрошки

Варианты

Ссылки или настоящие вкладки — два разных случая, и разметка у них разная. Выбирается тот, который правдив:

Случай Разметка Признак
Переключение экрана <nav class="inst-tabs"> + <a class="inst-tab" aria-current="page"> У каждого вида свой адрес. Средний клик открывает вкладку в новой вкладке браузера
Панели внутри одной страницы role="tablist" + role="tab" + aria-selected + aria-controls Адрес не меняется, содержимое подменяется на месте

Оформление ловит оба: селектор кита реагирует и на [aria-current]:not([aria-current="false"]), и на [aria-selected="true"]. Поэтому вопрос «какой класс поставить» не возникает — возникает вопрос «какая разметка правдива».

Настоящие вкладки

role="tablist" ставится только если вкладки настоящие. Роль обещает скринридеру стрелочную навигацию и связь с панелью; необеспеченное обещание хуже отсутствующего.

Пример
Вершин 12 480, полигонов 6 240.
Разметка
<div class="inst-tabs" role="tablist" aria-label="Свойства объекта">
  <button class="inst-tab" type="button" role="tab" id="t-geom"
          aria-selected="true" aria-controls="p-geom" tabindex="0">Геометрия</button>
  <button class="inst-tab" type="button" role="tab" id="t-mat"
          aria-selected="false" aria-controls="p-mat" tabindex="-1">Материал</button>
  <button class="inst-tab" type="button" role="tab" id="t-phys"
          aria-selected="false" aria-controls="p-phys" tabindex="-1">Физика</button>
</div>
<div id="p-geom" role="tabpanel" aria-labelledby="t-geom" tabindex="0">Вершин 12 480, полигонов 6 240.</div>
<div id="p-mat" role="tabpanel" aria-labelledby="t-mat" tabindex="0" hidden>Стандартный PBR, две текстуры.</div>
<div id="p-phys" role="tabpanel" aria-labelledby="t-phys" tabindex="0" hidden>Выпуклая оболочка, масса 4,2 кг.</div>

Пример живой: войдите Tab и нажмите или . Отметка переносится вместе с фокусом, Home и End работают, перебор идёт по кругу.

Стрелки, Home, End и бегущий tabindex выполняет kit.js. Показать нужную панель — приложение: какая панель за какой вкладкой, кит не знает.

Состояния

Пример
Разметка
<nav class="inst-tabs" aria-label="Пример активной вкладки">
  <a class="inst-tab" href="#" aria-current="page">Активная</a>
  <a class="inst-tab" href="#">Обычная</a>
  <a class="inst-tab" href="#">Ещё одна</a>
</nav>
Признак активной Что даёт
Подчёркивание border-block-end шириной --size-marker цветом --accent-solid. Носитель наравне с цветом
Цвет подписи --text-primary вместо --text-secondary
Начертание --weight-medium

Полоса вкладки перекрывает линию контейнера отрицательным полем в --hairline, а не сдвигом на полпикселя: браузер не рисует рамку тоньше физического пикселя, и остаток был бы виден на каждом стыке.

JS

Подключите модуль один раз на страницу.

<script type="module" src="src/kit.js"></script>

Что делает кит

Пример в разделе «Настоящие вкладки» живой: , , Home, End, перебор по кругу.

Полосу объявляет role="tablist", и кит выполняет контракт этой роли: и между вкладками, Home, End, перебор по кругу, один Tab на всю полосу. aria-selected переносится и стрелкой, и щелчком.

События

inst:select всплывает с выбранной вкладки, detail{ value }: data-value, если он есть, иначе подпись.

tabs.addEventListener('inst:select', (e) => {
  for (const panel of panels) {
    panel.hidden = panel.id !== e.detail.value;
  }
});

Что остаётся приложению

Показать нужную панель и спрятать остальные. Какая панель за какой вкладкой, знает только приложение — кит видит разметку, а не смысл.

Композиции

С иконкой и счётчиком

Пример
Разметка
<nav class="inst-tabs" aria-label="Пример со счётчиком">
  <a class="inst-tab" href="#" aria-current="page">
    <svg class="inst-icon" aria-hidden="true"><use href="#i-list"/></svg>Очередь
    <span class="inst-badge">7</span></a>
  <a class="inst-tab" href="#">История</a>
</nav>

Зазор внутри вкладки — --space-3, между вкладками — --space-6: расстояние между соседями обязано быть заметно больше, чем внутри одной, иначе иконка следующей читается как часть предыдущей.

Правила

Так Роутинг на ссылках

Каждый вид — свой адрес. Средний клик, история браузера и копирование ссылки работают без JS.

Не так role="tablist" без стрелочной навигации

Роль обещает скринридеру стрелки и связь с панелью. Необеспеченное обещание хуже отсутствующего.

Так Активная вкладка подчёркиванием

Носитель наравне с цветом. aria-current или aria-selected сообщает то же самое скринридеру.

Не так Вкладки вместо выбора значения

Переключение вида — переход; выбор одного из равных значений — ввод, то есть сегментированный контрол.

Доступность

Клавиатура, роутинг Нативные ссылки: Tab — обход, Enter — переход
Клавиатура, настоящие вкладки Tab входит в полосу один раз, стрелки переключают, — это JS приложения. Без него role="tablist" ставить нельзя
Цвет не единственный носитель Активная вкладка несёт подчёркивание и aria-current / aria-selected
Прокрутка Полоса прокручивается по горизонтали, скроллбар скрыт визуально, но прокрутка колесом и с клавиатуры остаётся
Контраст Неактивная подпись --text-secondary держит 4.5:1 в шести темах
Фокус Кольцо :focus-visible от базового слоя, полоса вкладок его не обрезает

API

ИмяЗначениеЧто делает
класс
inst-tabsПолоса вкладок. Нижняя линия и горизонтальная прокрутка без видимого скроллбара
inst-tabВкладка. Ссылка или кнопка с role="tab"
токен
--space-36px
--space-48px
--space-616px
--text-sm0.8125rem
--weight-medium500
--size-marker2px
--hairline1px
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))
--accent-solidvar(--a-4)
--text-primarylight-dark(var(--n-12), var(--n-1))
--text-secondarylight-dark(var(--n-9), var(--n-5))

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