Отображение данных
Таблица
Сравнимые записи в колонках. Настоящая <table> — с семантикой заголовков,
навигацией скринридера по ячейкам и печатью.
| Прогон | Модель | Время |
|---|---|---|
| worldgen-01 | opus | 2,1 с |
| biomes-04 | sonnet | 8,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), и второе существует затем, чтобы действие, видимое только под курсором, не пропадало для клавиатуры; - выделите строку и наведите на неё — подсветки складываются, потому что выделение полупрозрачно;
- прокрутите тело панели — шапка останется на месте.
| Состояние | Действия | ||||
|---|---|---|---|---|---|
| Рендер | готово | 18,2 | 61 204 | ||
| Физика | готово | 11,5 | 38 910 | ||
| Аудио | упало | 4,2 | 1 180 | ||
| Итого | 33,9 | 101 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-sort | ascending · descending | на <th> |
aria-selected | true · false | на <tr> |
| переменная | ||
--table-head-bg | --surface-raised | |
| токен | ||
--text-sm | 0.8125rem | |
--pad-cell-x | var(--space-4) | |
--row-pad-y | var(--space-3) | |
--space-3 | 6px | |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | |
--border-subtle | light-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06)) | |
--hairline | 1px | |
--text-muted | light-dark(var(--n-8), var(--n-6)) | |
--surface-selected | color-mix(in oklab, var(--a-4) 14%, transparent) | |
--surface-hover | light-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045)) | |
--control-h-md | 32px | |
--z-sticky | 10 | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument