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

Отображение данных

Таблица

JS Сортировка и действия строки. «Выбрать всё» делает кит

Сравнимые записи в колонках. Настоящая <table> — с семантикой заголовков, навигацией скринридера по ячейкам и печатью.

Компонент
ПрогонМодельВремя
worldgen-01opus2,1 с
biomes-04sonnet8,4 с
Разметка
<table class="inst-table">
  <thead>
    <tr><th>Прогон</th><th>Модель</th><th class="inst-num">Время</th></tr>
  </thead>
  <tbody>
    <tr><td>worldgen-01</td><td>opus</td><td class="inst-num">2,1 с</td></tr>
    <tr><td>biomes-04</td><td>sonnet</td><td class="inst-num">8,4 с</td></tr>
  </tbody>
</table>

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

<table class="inst-table">
  <caption>Модули прогона</caption>
  <thead>
    <tr><th>Прогон</th><th class="inst-num">Время</th></tr>
  </thead>
  <tbody>
    <tr><td>worldgen-01</td><td class="inst-num">2,1 с</td></tr>
  </tbody>
</table>
Что Обязательно Почему
Настоящая <table> с <th> в <thead> да Скринридер озвучивает заголовок при переходе по ячейкам — без него таблица превращается в набор чисел
<caption> или aria-label да Две таблицы на экране без имён неразличимы на слух
inst-num на числовых ячейках да, у чисел Выравнивание по концу строки: без него разряды не сравнить глазом
Достижимая с клавиатуры область прокрутки да, при горизонтальной прокрутке Иначе часть колонок существует только для мыши

Разделение труда. «Выбрать всё» делает kit.js, включая частичный выбор. Сортировка и действия строки — слой приложения: порядок строк это данные, и кит их не знает.

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

Используйте Возьмите другое
Записи, которые сравнивают по одним и тем же полям Разнородные объектыкарточки: в таблице половина ячеек будет пустой
Больше трёх полей на запись Пара «имя — значение»список свойств
Нужны сортировка, выбор строк, итоги Строки с состоянием и прогрессомочередь: она плоская и не имеет колонок
Данные уйдут в печать Десятки тысяч строк — нужна виртуализация, её в ките нет

Устройство

Числовые колонки

<th class="inst-num">Время</th>
<td class="inst-num">2,1 с</td>

inst-num выравнивает по концу строки. Табличные цифры включены по умолчанию для всего кита, поэтому разряды встают друг под друга без дополнительных усилий.

Итоги

<tfoot>
  <tr><td>Всего</td><td></td><td class="inst-num">10,5 с</td></tr>
</tfoot>

<tfoot> идёт в разметке после <tbody> и отбивается сверху. Строка итогов — не последняя строка данных, и путать их нельзя.

Состояния

Выбор строк

Пример
Прогон
worldgen-01
biomes-04
Разметка
<table class="inst-table">
  <thead><tr><th class="inst-col-select"><label class="inst-checkbox"><input type="checkbox" aria-label="Выбрать все"></label></th><th>Прогон</th></tr></thead>
  <tbody>
    <tr aria-selected="true">
      <td class="inst-col-select"><label class="inst-checkbox"><input type="checkbox" checked aria-label="Выбрать worldgen-01"></label></td>
      <td>worldgen-01</td>
    </tr>
    <tr>
      <td class="inst-col-select"><label class="inst-checkbox"><input type="checkbox" aria-label="Выбрать biomes-04"></label></td>
      <td>biomes-04</td>
    </tr>
  </tbody>
</table>

Состояние живёт на aria-selected у <tr>. Подсветка полупрозрачна, поэтому состояния складываются: наведение на выбранную строку остаётся видно.

Поведение

Сортировка

<th aria-sort="ascending">
  <button class="inst-sort" type="button">Время</button>
</th>
Значение aria-sort Что показывает
нет атрибута Колонка не сортирована
ascending По возрастанию, значок вверх
descending По убыванию, значок вниз

Направление несёт значок, и то же самое озвучивает aria-sort. Одной позиции строк мало. Заголовок-сортировка — настоящая <button> внутри <th>, иначе до неё не добраться с клавиатуры.

У числовой колонки значок идёт перед подписью, иначе он отрывается от неё на всю ширину ячейки.

Действия строки

<td class="inst-col-actions">
  <div class="inst-row-actions">
    <button class="inst-btn inst-btn--sm inst-btn--icon" type="button" aria-label="Обновить">
      <svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
    </button>
  </div>
</td>

Действия появляются на наведении — и всегда доступны с клавиатуры: они проявляются также по :focus-within и на выбранной строке. Действие, видимое только под курсором, для клавиатуры не существует.

Липкие шапка и колонка

<table class="inst-table inst-table--sticky inst-table--sticky-col">
Класс Что липнет
inst-table--sticky Шапка при вертикальной прокрутке
inst-table--sticky-col Первая колонка при горизонтальной

Липкость работает относительно прокручиваемого предка — обычно inst-panel-body. Таблица в неприкреплённом потоке ничего не приклеит.

JS

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

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

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

Пример в разделе «Состояния» живой: отметьте чекбокс в шапке — выберутся все строки, снимите одну — он станет промежуточным.

Только чекбокс «выбрать всё» в шапке колонки выбора:

  • отмечает и снимает все строки, кроме disabled;
  • держит aria-selected на <tr>;
  • показывает частичный выбор через indeterminate — «часть строк выбрана» невыразимо ни через checked, ни через его отсутствие;
  • пересчитывает себя, когда строку отметили поодиночке.

События

inst:selectall всплывает с таблицы, detail{ checked }. Отмена оставляет строки как есть.

table.addEventListener('inst:selectall', (e) => {
  bulkBar.hidden = !e.detail.checked;
});

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

Что Почему не кит
Сортировка Порядок строк — это данные. Кит рисует aria-sort и стрелку, переставляет строки приложение
Действия строки Что делает «Остановить» — знает только приложение
Пагинация и виртуализация Зависят от источника данных

Сценарии

Таблица в панели

Таблица показывает себя только целиком: сортировка, выбор строк, действия и итоги по отдельности выглядят как четыре независимых класса, а вместе — как один инструмент.

Что проверить руками:

  • наведите на строку — появятся действия. Теперь пройдите по строкам Tab: действия появятся снова. Это разные события (:hover и :focus-within), и второе существует затем, чтобы действие, видимое только под курсором, не пропадало для клавиатуры;
  • выделите строку и наведите на неё — подсветки складываются, потому что выделение полупрозрачно;
  • прокрутите тело панели — шапка останется на месте.
В контексте
Модули прогона выбрано 1
Состояние Действия
Рендер готово 18,261 204
Физика готово 11,538 910
Аудио упало 4,21 180
Итого33,9101 294
Разметка
<div class="inst-panel">
  <div class="inst-panel-header">
    <span class="inst-panel-title">Модули прогона</span>
    <span class="inst-panel-actions"><span class="inst-badge">выбрано 1</span></span>
  </div>
  <div class="inst-panel-body inst-panel-body--flush">
    <table class="inst-table inst-table--sticky">
      <thead>
        <tr>
          <th class="inst-col-select"><label class="inst-checkbox"><input type="checkbox" aria-label="Выбрать все строки"></label></th>
          <th aria-sort="descending"><button class="inst-sort" type="button">Модуль</button></th>
          <th>Состояние</th>
          <th class="inst-num"><button class="inst-sort" type="button">Время, с</button></th>
          <th class="inst-num"><button class="inst-sort" type="button">Токенов</button></th>
          <th class="inst-col-actions"><span class="inst-u-visually-hidden">Действия</span></th>
        </tr>
      </thead>
      <tbody>
        <tr aria-selected="true">
          <td><label class="inst-checkbox"><input type="checkbox" checked aria-label="Выбрать «Рендер»"></label></td>
          <td>Рендер</td>
          <td><span class="inst-badge" data-tone="ok"><span class="inst-dot"></span>готово</span></td>
          <td class="inst-num">18,2</td><td class="inst-num">61 204</td>
          <td class="inst-col-actions"><span class="inst-row-actions">
            <button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon" type="button" aria-label="Перезапустить Рендер">
              <svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg></button>
          </span></td>
        </tr>
        <tr>
          <td><label class="inst-checkbox"><input type="checkbox" aria-label="Выбрать «Физика»"></label></td>
          <td>Физика</td>
          <td><span class="inst-badge" data-tone="ok"><span class="inst-dot"></span>готово</span></td>
          <td class="inst-num">11,5</td><td class="inst-num">38 910</td>
          <td class="inst-col-actions"><span class="inst-row-actions">
            <button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon" type="button" aria-label="Перезапустить Физика">
              <svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg></button>
          </span></td>
        </tr>
        <tr>
          <td><label class="inst-checkbox"><input type="checkbox" aria-label="Выбрать «Аудио»"></label></td>
          <td>Аудио</td>
          <td><span class="inst-badge" data-tone="error"><span class="inst-dot"></span>упало</span></td>
          <td class="inst-num">4,2</td><td class="inst-num">1 180</td>
          <td class="inst-col-actions"><span class="inst-row-actions">
            <button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon" type="button" aria-label="Перезапустить Аудио">
              <svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg></button>
          </span></td>
        </tr>
      </tbody>
      <tfoot>
        <tr><td></td><td>Итого</td><td></td><td class="inst-num">33,9</td><td class="inst-num">101 294</td><td></td></tr>
      </tfoot>
    </table>
  </div>
</div>

Правила

Так Настоящая table с th в thead

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

Не так Сетка из div с role="grid"

Она теряет навигацию по ячейкам, печать и половину поведения, которое таблица даёт бесплатно.

Так Действия и по focus-within

Действие, видимое только под курсором, для клавиатуры не существует.

Не так Таблица для разнородных объектов

Половина ячеек окажется пустой — это карточки.

Доступность

Заголовки <th> в <thead>. Скринридер озвучивает заголовок при переходе по ячейкам — без него таблица превращается в набор чисел
Сортировка aria-sort на <th> плюс <button> внутри. Атрибут без кнопки недостижим, кнопка без атрибута не озвучивает направление
Выбор aria-selected на <tr>. Чекбокс несёт своё имя, иначе колонка озвучится как «отметка, отметка, отметка»
Действия строки Проявляются по :focus-within, а не только по :hover. Иначе они существуют лишь для мыши
Подпись таблицы <caption> или aria-label. Две таблицы на экране без имён неразличимы на слух
Не только цвет Выбранная строка несёт aria-selected, а не одну лишь подсветку
Горизонтальная прокрутка Область прокрутки должна быть достижима с клавиатуры

API

ИмяЗначениеЧто делает
класс
inst-tableБазовый, на <table>
inst-numЧисловая ячейка: выравнивание по концу
inst-sortКнопка-заголовок сортировки
inst-col-selectКолонка чекбоксов, ширина по контролу
inst-col-actionsКолонка действий, ширина по содержимому
inst-row-actionsОбёртка действий: проявляется на наведении и фокусе
модификатор
inst-table--stickyЛипкая шапка
inst-table--sticky-colЛипкая первая колонка
атрибут
aria-sortascending · descendingна <th>
aria-selectedtrue · falseна <tr>
переменная
--table-head-bg--surface-raised
токен
--text-sm0.8125rem
--pad-cell-xvar(--space-4)
--row-pad-yvar(--space-3)
--space-36px
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))
--border-subtlelight-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06))
--hairline1px
--text-mutedlight-dark(var(--n-8), var(--n-6))
--surface-selectedcolor-mix(in oklab, var(--a-4) 14%, transparent)
--surface-hoverlight-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045))
--control-h-md32px
--z-sticky10

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