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

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

Таблица

JS Сортировка и действия строки. «Выбрать всё» делает `instrument.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> с <th> в <thead> да Скринридер озвучивает заголовок при переходе по ячейкам — без него таблица превращается в набор чисел
<caption> или aria-label да Две таблицы на экране без имён неразличимы на слух
inst-num на числовых ячейках да, у чисел Выравнивание по концу строки: без него разряды не сравнить глазом
Достижимая с клавиатуры область прокрутки да, при горизонтальной прокрутке Иначе часть колонок существует только для мыши

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

Доступность

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

Устройство

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

<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

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

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

Что делает instrument.js

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

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

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

События

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

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

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

Что Почему не instrument.js
Сортировка Порядок строк — это данные. Библиотека рисует 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>

Ячейка-плитка

Пример
ПрогонВеткаМодельТокенов
nightly-4127mainopus186 000
audio-passfix/mixdownsonnet42 300
biomes-04mainopus98 700
Разметка
<table class="inst-table inst-table--cells">
  <thead><tr><th>Прогон</th><th>Ветка</th><th>Модель</th><th class="inst-num">Токенов</th></tr></thead>
  <tbody>
    <tr><td>nightly-4127</td><td>main</td><td>opus</td><td class="inst-num">186 000</td></tr>
    <tr><td>audio-pass</td><td>fix/mixdown</td><td>sonnet</td><td class="inst-num">42 300</td></tr>
    <tr><td>biomes-04</td><td>main</td><td>opus</td><td class="inst-num">98 700</td></tr>
  </tbody>
</table>

Линейка отделяет строку от строки и ничего не говорит про колонку: на пятнадцати колонках глаз, идущий вправо, теряет и строку, и границу значения. Плитка держит и то и другое сразу — у каждого значения своя коробка.

Шапка плиткой не становится: она не значение, а имя колонки, и коробка вокруг имени сделала бы её равной данным.

Полосы вместо линеек

Пример
ПрогонВеткаМодельТокеновВремя
nightly-4127mainopus186 00021 с
audio-passfix/mixdownsonnet42 3004,2 с
biomes-04mainopus98 70011,5 с
terrain-88mainsonnet150 40018,2 с
Разметка
<table class="inst-table inst-table--zebra">
  <thead><tr><th>Прогон</th><th>Ветка</th><th>Модель</th><th class="inst-num">Токенов</th><th class="inst-num">Время</th></tr></thead>
  <tbody>
    <tr><td>nightly-4127</td><td>main</td><td>opus</td><td class="inst-num">186 000</td><td class="inst-num">21 с</td></tr>
    <tr><td>audio-pass</td><td>fix/mixdown</td><td>sonnet</td><td class="inst-num">42 300</td><td class="inst-num">4,2 с</td></tr>
    <tr><td>biomes-04</td><td>main</td><td>opus</td><td class="inst-num">98 700</td><td class="inst-num">11,5 с</td></tr>
    <tr><td>terrain-88</td><td>main</td><td>sonnet</td><td class="inst-num">150 400</td><td class="inst-num">18,2 с</td></tr>
  </tbody>
</table>

Линейка под каждой строкой работает, пока колонок пять. На пятнадцати она отделяет строку от соседа, но не показывает, где вы внутри строки, — глаз уезжает по вертикали, пока идёт вправо. Заливка через одну показывает: она тянется на всю ширину и не пропадает при горизонтальной прокрутке.

Вместе с линейками её ставить нельзя: два разделителя на одной границе дают решётку. Поэтому вариант линейки снимает, а не дополняет.

Берите полосы Берите линейки
Колонок больше семи, таблицу прокручивают вбок Колонок до пяти, строка видна целиком
Строки читают целиком, слева направо Строки сравнивают между собой по одной колонке
Таблица длинная и стоит на своей поверхности Таблица короткая и лежит внутри карточки

API

Классы
ИмяЧто делает
класс
inst-tableБазовый, на <table>
inst-numЧисловая ячейка: выравнивание по концу
inst-sortКнопка-заголовок сортировки
inst-col-selectКолонка чекбоксов, ширина по контролу
inst-col-actionsКолонка действий, ширина по содержимому
inst-row-actionsОбёртка действий: проявляется на наведении и фокусе
модификатор
inst-table--cellsЯчейка-плитка: каждое значение в своей коробке, между коробками просвет
inst-table--zebraПолосы вместо линеек. Для широкой таблицы: на пятнадцати колонках линейка не показывает, ГДЕ ты внутри строки
inst-table--stickyЛипкая шапка
inst-table--sticky-colЛипкая первая колонка
Атрибуты
ИмяЗначениеЧто делает
атрибут
aria-sortascending · descendingна <th>
aria-selectedtrue · falseна <tr>
Токены
ИмяЗначениеОткуда
переменная
--table-head-bg--surface-raisedзадаёт приложение
токен
--text-sm0.875remбаза +4: масштаб
--pad-cell-xvar(--space-4)база +14: плотность · масштаб
--row-pad-yvar(--space-3)база +14: плотность · масштаб
--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база +1: экран
--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база +14: плотность · масштаб
--z-sticky10одно на все ячейки

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