Тег th (от английского table header, «заголовок таблицы») создаёт заголовок таблицы: название колонки, строки или целой группы строк. На экране такая ячейка отличается от td только жирным шрифтом и центрированием, а главное отличие скрыто: скринридер при переходе по ячейкам сообщает заголовок, и по нему человек понимает, что за число он слышит.
Основы тега и атрибута scope разобраны в уроке «Тег th и атрибут scope: заголовки колонок и строк таблицы». Здесь разберём остальное: как браузер определяет роль заголовка без scope, чем headers отличается от scope, как собрать многоуровневую шапку, почему выравнивание th не сдвигается от некоторых значений родителя, как закрепить шапку вместе с колонкой и как сделать заголовки сортируемыми.
<table>
<tr>
<th scope="col">Чай</th>
<th scope="col">Цена</th>
</tr>
<tr>
<th scope="row">Сенча</th>
<td>100</td>
</tr>
</table>
В этой статье
- Тег th в HTML: что это и чем отличается от td
- Атрибут scope у th: col, row, colgroup и rowgroup
- Атрибут headers и id: сложные заголовки th
- Атрибуты th: abbr, устаревшие и что нельзя внутри
- Многоуровневая шапка th: colspan и rowspan
- Стили th: выравнивание, жирность и выбор th в CSS
- Длинные и вертикальные заголовки th в узкой таблице
- Закрепление шапки и колонки th: угловая ячейка
- Тег th и JavaScript: scope, createElement и сортировка
- Частые ошибки с тегом th
Тег th в HTML: что это и чем отличается от td
Ячейка th записывается только внутри строки tr. Закрывающий </th> можно опустить: ячейка кончается там, где начинается следующая th или td. Если записать th прямо в table, валидатор Nu сообщит об ошибке, а браузер сам достроит tbody и tr. Внутри допустимы абзацы, ссылки, кнопки и другие элементы потока, кроме заголовков, header, footer и секций.
Роль ячейки Chrome определяет по положению. th в строке шапки становится заголовком колонки (роль columnheader), а первая th в строке тела или подвала — заголовком строки (роль rowheader). Поэтому подмена не проходит ни в одну сторону: td со стилями заголовка остаётся ячейкой данных, а th, записанная ради жирного шрифта в ячейке с числом, превращает данные в заголовок.
| Запись | Что получается | Пример записи |
|---|---|---|
th в строке шапки | Заголовок колонки, жирный текст по центру | <th>Цена</th> |
Первая th в строке тела | Заголовок строки | <tr><th>Сенча</th><td>100</td></tr> |
td со стилями заголовка | Выглядит как заголовок, для скринридера это данные | td { font-weight: 700; } |
th вместо td ради жирности | Данные становятся заголовком | <tr><td>Сенча</td><th>100</th></tr> |
Пустая th в углу таблицы | Допустима: закрывает угол над заголовками строк | <th></th> |
th прямо в table | Браузер достраивает tbody и tr, валидатор считает ошибкой | <table><th>Цена</th></table> |
Пример: чем th отличается от td
Выберите запись: вверху виден код, под ним — что получилось на экране.
Допустимое содержимое и места записи описаны в стандарте HTML.
Атрибут scope у th: col, row, colgroup и rowgroup
Атрибут scope говорит, на какие ячейки действует заголовок. Значение col задаёт колонку, row — строку, colgroup и rowgroup — группу колонок или строк: их записывают у th, которая охватывает несколько колонок или строк через colspan или rowspan. Без атрибута браузер решает по положению ячейки, и для простых таблиц этого хватает.
Явный scope полезен там, где положение может подсказать неверно: таблица с заголовками и в шапке, и в первой колонке, угловая ячейка, итоговая строка в подвале. Допустимы только четыре значения. Запись scope="auto", хотя так называется состояние по умолчанию в стандарте, валидатор считает ошибкой, как и scope="column" или пустое значение. У td атрибут scope устарел и ничего не меняет: ячейка остаётся ячейкой данных.
| Запись | Что получается | Пример записи |
|---|---|---|
scope="col" | Заголовок относится к ячейкам колонки под ним | <th scope="col">Цена</th> |
scope="row" | Заголовок относится к ячейкам строки правее него | <th scope="row">Сенча</th> |
scope="colgroup" | Заголовок относится к группе колонок | <th scope="colgroup" colspan="2">Цена</th> |
scope="rowgroup" | Заголовок относится к группе строк | <th scope="rowgroup" rowspan="2">Зелёный</th> |
Без scope | Охват по положению: в шапке колонка, первая в строке тела строка | <th>Цена</th> |
scope="auto", "column", пустое | Недопустимы: валидатор считает ошибкой | <th scope="column">Цена</th> |
scope у td | Устарел и игнорируется | <td scope="row">Сенча</td> |
Пример: на какие ячейки действует scope
Выберите значение: вверху виден код, под ним таблица. Подсвечены ячейки, к которым относится заголовок по правилам стандарта.
Атрибут headers и id: сложные заголовки th
Если у ячейки несколько заголовков разных уровней, scope их не опишет. Тогда каждой th задают id, а ячейке данных в атрибуте headers перечисляют через пробел все заголовки, к которым она относится. Типичный случай: шапка с ячейкой Цена, которая занимает две колонки, и под ней подзаголовки Опт и Розница. Ячейка со значением должна указать оба уровня: headers="pr opt".
Правила строгие. В списке допустимы только id ячеек th из той же таблицы: ссылка на td или на несуществующий id считается ошибкой, повторяющийся id тоже. Если указан один уровень, со вторым ячейка не связана. При наличии headers по стандарту связь строят по этому списку, а не по положению ячейки. Как headers выглядит на ячейке данных, видно и на странице td.
| Запись | Что получается | Пример записи |
|---|---|---|
id у th | Метка для ссылки из headers | <th id="pr">Цена</th> |
headers с обоими уровнями | Ячейка связана с Цена и Опт | <td headers="pr opt">90</td> |
headers с одним уровнем | Верхний уровень теряется | <td headers="opt">90</td> |
headers на td | Валидатор считает ошибкой | <td headers="t1">90</td> |
headers на несуществующий id | Валидатор считает ошибкой | <td headers="net">90</td> |
headers у th | Допустим: заголовок второго уровня ссылается на верхний | <th id="opt" headers="pr">Опт</th> |
Пример: ячейка и её заголовки
Выберите запись и нажмите на любую ячейку тела: подсветятся заголовки, на которые она ссылается.
<tr>
<th rowspan="2" id="ch">Чай</th>
<th colspan="2" id="pr">Цена</th>
</tr>
<tr>
<th id="opt">Опт</th>
<th id="rz">Розница</th>
</tr>
<tr>
<td headers="ch">Сенча</td>
<td headers="pr opt">90</td>
<td headers="pr rz">100</td>
</tr>
Атрибуты th: abbr, устаревшие и что нельзя внутри
Кроме scope, headers, colspan и rowspan, у th есть abbr: сокращённое название заголовка, которое скринридер может читать вместо длинного. В Chrome имя ячейки для скринридера остаётся полным текстом, поэтому abbr служит подсказкой, а не заменой короткого текста в самой ячейке. У td этот атрибут устарел.
Устарели атрибуты width, height, align, valign, bgcolor, nowrap, а также axis (его заменил scope), char и charoff. Chrome их выполняет, валидатор предупреждает. Вместо них пишут свойства CSS. Роль писать не нужно: любой role у th, даже columnheader, валидатор считает ошибкой. Допустимы глобальные id, class, tabindex, hidden, а также aria-sort: на th он разрешён, а на td нет.
| Запись | Что получается | Пример записи |
|---|---|---|
abbr у th | Допустим: сокращённое название для скринридера | <th abbr="Цена">Цена за 100 г</th> |
abbr, scope у td | Устарели, валидатор предупреждает | Убрать атрибут |
width, height, align, valign | Устарели: применяются, валидатор предупреждает | Свойства width, height, text-align, vertical-align |
bgcolor, nowrap | Устарели: применяются, валидатор предупреждает | Свойства background, white-space |
axis, char, charoff | Устарели, валидатор предупреждает | Для axis писать scope |
role="columnheader" | Валидатор считает ошибкой: роль у th уже есть | Убрать атрибут |
aria-sort у th | Допустим: отмечает отсортированную колонку | <th aria-sort="ascending"> |
colspan="0" | Валидатор считает ошибкой | <th colspan="2">Цена</th> |
rowspan="0" | Допустим: до конца секции | <th rowspan="0">Чай</th> |
p, button внутри th | Допустимы | <th><button type="button">Цена</button></th> |
h3, header, section внутри th | Валидатор считает ошибкой | <th><h3>Цена</h3></th> |
Многоуровневая шапка th: colspan и rowspan
Ступенчатая шапка из двух строк собирается так: верхняя ячейка с colspan накрывает подзаголовки, а ячейка слева с rowspan занимает обе строки шапки. Если забыть rowspan, подзаголовки второй строки сдвигаются влево под колонку первой ячейки, а данные расходятся с шапкой. Значение rowspan="0" растягивает ячейку до конца секции, в тело оно не заходит.
То же работает и в тело таблицы: th с rowspan и scope="rowgroup" задаёт название группы строк, а в каждой строке остаётся своя th со scope="row". Пределы значений colspan и rowspan описаны на странице td: у th они те же.
| Запись | Что получается | Пример записи |
|---|---|---|
colspan над подзаголовками | Ячейка шириной в две колонки | <th colspan="2">Цена</th> |
rowspan у левой ячейки | Ячейка высотой в две строки шапки | <th rowspan="2">Чай</th> |
Забыт rowspan | Подзаголовки сдвигаются под первую колонку | <th>Чай</th><th colspan="2">Цена</th> |
rowspan="0" | Ячейка до конца секции thead | <th rowspan="0">Чай</th> |
colspan больше числа колонок | Лишние колонки без ширины, валидатор считает ошибкой | <th colspan="5">Цена</th> |
rowspan и scope="rowgroup" | Заголовок группы строк | <th rowspan="2" scope="rowgroup">Зелёный</th> |
Пример: шапка из двух уровней
Выберите запись: вверху виден код, под ним таблица и размеры ячеек, которые считает сама страница.
Стили th: выравнивание, жирность и выбор th в CSS
Жирность и центрирование заданы правилом самого браузера для th, а не наследуются от строки. Из этого вытекают две ловушки. Значения left или right у tr, thead и table сдвигают заголовки, а text-align: start у родителя нет: оно совпадает с начальным значением, и заголовок остаётся по центру. Чтобы выровнять заголовки, правило пишут на самой th: th { text-align: start; } даёт ожидаемый результат.
То же с жирностью: font-weight: 400 у tr шрифт th не меняет, нужно правило на самой ячейке. Наследование остальных свойств строки подробно описано на странице tr. Шапку, колонку заголовков и ячейки данных различают селекторы thead th, tbody th и th[scope="row"]: во втором и третьем случае правило не затрагивает шапку.
| Запись | Что получается | Пример записи |
|---|---|---|
| Без правил | Жирный шрифт, выравнивание по центру | th { } |
text-align: left у tr | Заголовки выровнены влево | tr { text-align: left; } |
text-align: start у tr | Не действует: заголовки остаются по центру | tr { text-align: start; } |
font-weight: 400 у tr | Не действует: заголовки жирные | tr { font-weight: 400; } |
text-align: start у th | Выравнивание по началу строки | th { text-align: start; } |
thead th, tbody th | Отдельный вид для шапки и колонки заголовков | tbody th { text-align: left; } |
th[scope="row"] | Выбор заголовков строк по атрибуту | th[scope="row"] { background: #cfe8e4; } |
Пример: что действует на th
Выберите правило: вверху виден код, под ним таблица и значения, которые считает сама страница.
th {
text-align: start;
font-weight: 600;
}
thead th {
background: #ffd9a8;
}
th[scope="row"] {
background: #cfe8e4;
}
Длинные и вертикальные заголовки th в узкой таблице
Заголовок без пробелов задаёт колонке минимальную ширину, и таблица выходит за свой блок. Ширину возвращает overflow-wrap: anywhere: слово переносится по буквам. Другой путь: оставить заголовок в одну строку правилом white-space: nowrap и дать таблице блок с overflow-x: auto, тогда прокручивается блок, а не страница. Остальные приёмы с длинным словом и многоточием описаны на странице td.
Для узких колонок с длинными названиями заголовок поворачивают: writing-mode: vertical-rl делает текст вертикальным, и ячейка сужается до ширины одной строки текста. Текст читается сверху вниз. Если нужно снизу вверх, добавляют transform: rotate(180deg), но размер ячейки при этом остаётся прежним.
| Запись | Что получается | Пример записи |
|---|---|---|
Слово без пробелов в th | Колонка и таблица растягиваются | <th>Среднесуточнаякалорийность</th> |
overflow-wrap: anywhere | Слово переносится по буквам | th { overflow-wrap: anywhere; } |
white-space: nowrap и блок | Заголовок в одну строку, прокрутка в блоке | th { white-space: nowrap; } |
writing-mode: vertical-rl | Вертикальный текст, ячейка уже | th { writing-mode: vertical-rl; } |
transform: rotate(180deg) | Текст снизу вверх, размер прежний | th { transform: rotate(180deg); } |
Пример: длинные и повёрнутые заголовки
Выберите правило: вверху виден код, под ним таблица в блоке шириной до 180 px и размеры, которые считает сама страница.
Закрепление шапки и колонки th: угловая ячейка
В большой таблице закрепляют сразу шапку и колонку заголовков: thead th получает position: sticky и top: 0, tbody th получает position: sticky и left: 0, а таблицу оборачивают в блок с overflow: auto и фиксированной высотой. Закрепление одной шапки и повтор шапки на каждой печатной странице описаны на странице thead, а закрепление подвала с итогами — на странице tfoot.
У связки есть ловушка с углом. Колонка заголовков стоит в разметке позже шапки и при прокрутке ложится поверх неё: слева вверху вместо названия угла видна первая th колонки. Угловой ячейке задают left: 0 и z-index: 2. Только left: 0 перекрытия не лечит, а z-index у всей шапки без закрепления угла оставляет угол уехавшим. Фон у закреплённых ячеек обязателен, иначе содержимое просвечивает. Что происходит с границей закреплённой ячейки при border-collapse: collapse, показано на странице td.
| Запись | Что получается | Пример записи |
|---|---|---|
| Закреплена только шапка | Угловая ячейка уезжает вместе с колонками | thead th { position: sticky; top: 0; } |
| Закреплены шапка и колонка | Колонка перекрывает шапку слева вверху | tbody th { position: sticky; left: 0; } |
Угол с left: 0 без z-index | Перекрытие остаётся | thead th:first-child { left: 0; } |
z-index: 1 у всей шапки | Колонка под шапкой, но угол уехал | thead th { z-index: 1; } |
Угол с left: 0 и z-index: 2 | Угол на месте, шапка и колонка не пересекаются | thead th:first-child { left: 0; z-index: 2; } |
Пример: закрепление шапки и колонки
Выберите правило: вверху виден код, под ним блок с прокруткой, уже сдвинутый вниз и вправо.
thead th {
position: sticky;
top: 0;
background: #fde;
}
tbody th {
position: sticky;
left: 0;
background: #def;
}
thead th:first-child {
left: 0;
z-index: 2;
}
Тег th и JavaScript: scope, createElement и сортировка
Ячейка th в скрипте представлена тем же интерфейсом HTMLTableCellElement, что и td. Свойство scope принимает только допустимые значения: неверное оставляет в атрибуте написанное, но при чтении возвращает пустую строку, а регистр значения не важен. Свойства abbr и headers читаются как строки. Свойство cells возвращает th и td вместе, подробнее о нём на странице tr.
Метод insertCell() всегда создаёт td, даже в строке шапки, поэтому ячейку th создают через document.createElement('th') и добавляют методом append(). Чтобы превратить готовую строку из td в заголовки, каждую ячейку заменяют методом replaceWith().
| Запись | Что делает | Пример записи |
|---|---|---|
th.scope = "col" | Записывает значение в атрибут | th.scope = 'col' |
th.scope = "column" | Атрибут записан, чтение возвращает пустую строку | th.scope = 'column' |
row.insertCell(-1) в шапке | Создаёт td, а не th | row.insertCell(-1) |
createElement("th") | Создаёт ячейку th | document.createElement('th') |
td.replaceWith(th) | Заменяет ячейку данных на заголовок | td.replaceWith(th) |
row.cells | Возвращает th и td строки вместе | row.cells[0].tagName |
e.target.closest("th") | Находит заголовок, на который нажали | e.target.closest('th') |
Пример: свойства и создание th
Выберите действие: вверху виден код, под ним таблица и результат, который считает сама страница.
Заголовки колонок часто делают сортируемыми. Внутрь th помещают button, чтобы заголовок получал фокус и срабатывал по Enter, а у отсортированной колонки добавляют aria-sort: ascending, descending или none. Значение, отличное от none, задают только одной колонке, у остальных оно none. Без button заголовок мышью сортирует, но с клавиатуры до него не добраться.
Пример: сортировка по заголовку
Нажмите на заголовок колонки: строки отсортируются, повторное нажатие меняет направление.
table.addEventListener('click', (event) => {
const th = event.target.closest('th');
if (!th) return;
for (const other of table.tHead.rows[0].cells) {
other.setAttribute('aria-sort', 'none');
}
th.setAttribute('aria-sort', 'ascending');
// затем строки тела сортируются по колонке th.cellIndex
});
Частые ошибки с тегом th
Типичные ошибки с тегом th и как их исправить.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
td со стилями вместо th | Заголовок виден глазами, скринридер читает данные | Записывать заголовки в th |
th ради жирности в ячейке данных | Данные становятся заголовком | Оставить td, жирность задать в CSS |
scope="auto", "column" или пустой | Валидатор считает ошибкой, свойство возвращает пустую строку | Писать col, row, colgroup или rowgroup |
scope у td | Атрибут устарел и ничего не меняет | Перенести в th |
headers на td или несуществующий id | Валидатор считает ошибкой | Ссылаться на id ячеек th этой же таблицы |
headers с одним уровнем | Верхний заголовок теряется | Указывать все уровни через пробел |
role="columnheader" у th | Валидатор считает ошибкой | Убрать атрибут |
Атрибуты width, align, valign, bgcolor | Устарели, валидатор предупреждает | Свойства width, text-align, vertical-align, background |
h3, header или section внутри th | Валидатор считает ошибкой | Записывать текст, абзац или кнопку |
Забыт rowspan в двухуровневой шапке | Подзаголовки сдвигаются под первую колонку | Добавить rowspan="2" левой ячейке |
text-align: start у tr | Заголовки остаются по центру | th { text-align: start; } |
font-weight: 400 у tr | Заголовки остаются жирными | th { font-weight: 400; } |
| Закреплены шапка и колонка без угла | Колонка перекрывает шапку слева вверху | left: 0; z-index: 2; |
Длинное слово в th | Таблица шире блока | overflow-wrap: anywhere |
insertCell() в строке шапки | Создаётся td, а не th | document.createElement('th') |
Сортируемая th без button | Нет фокуса с клавиатуры | Поместить в th кнопку и добавить aria-sort |