Есть три способа обернуть кусок страницы: <div>, <section> и <article>. Визуально они не отличаются ничем — блочный элемент без единого дефолтного стиля. Поэтому выбор между ними в вёрстке часто делается по привычке или по тому, какое слово красивее смотрится в разметке.
Разница есть, но она невидимая: это три разных сообщения, которые страница отправляет в дерево доступности, браузерному режиму чтения и следующему разработчику. Разберём, чем именно они отличаются, как выбирать без долгих споров на код-ревью и что нужно дописать к <section>, чтобы от него был толк.
Что из этих тегов реально попадает в дерево доступности
Начнём с конца — с результата, а не с философии семантики. Браузер строит из HTML дерево доступности: параллельную структуру, в которой у каждого элемента есть роль, имя и состояние. Скринридер читает именно её, а не то, что видно на экране.
Возьмём один и тот же блок с карточками и обернём его четырьмя разными способами:
<div>...</div> <!-- роль generic -->
<section>...</section> <!-- роль generic, всё ещё -->
<section aria-label="Свежие вакансии">
...
</section> <!-- роль region -->
<article>...</article> <!-- роль article -->
Что из этого следует для пользователя:
| Разметка | Роль в дереве | Что это даёт пользователю |
| <div> | generic | Ничего. Элемент есть, но для скринридера он прозрачный. |
| <section> без доступного имени | generic | Тоже ничего. Отличий от <div> в озвучке нет. |
| <section> с доступным именем | region | Ориентир-лендмарк: попадает в список областей страницы, по нему есть быстрый переход. |
| <article> | article | Граница самостоятельного блока. Часть скринридеров объявляет её и умеет прыгать между такими блоками. |
Первый вывод из таблицы обычно удивляет: голый <section> ничем не полезнее <div>. Так задумано — иначе на типичной странице образовался бы десяток безымянных областей, между которыми невозможно ориентироваться. Роль region элемент получает, только когда у него появляется доступное имя (об этом ниже отдельный раздел).
Со вторым выводом сложнее: поддержка <article> у скринридеров неровная и зависит от связки программа + браузер. Где-то граница блока объявляется голосом и по ней работает отдельная горячая клавиша перехода, где-то элемент проходит молча, где-то озвучку можно включить в настройках, но по умолчанию она выключена. Универсального «работает везде» тут нет, и если поведение критично для интерфейса — его проверяют в конкретной связке, а не берут из таблицы в статье.

Отдельная деталь, на которой спотыкаются: в дереве доступности DevTools у безымянной секции может отображаться подпись section. Это не ARIA-роль, которую можно назначить руками. Роль section в спецификации ARIA абстрактная — она существует для внутренней классификации ролей, и писать role="section" в разметке нельзя.
Два вопроса вместо спора о семантике
Чтобы не гадать каждый раз, достаточно задать себе два вопроса подряд. Второй имеет смысл, только если на первый ответ утвердительный.
Вопрос первый: группировка вообще что-то значит?
Формулировка для проверки: если описывать структуру страницы словами, не показывая её, упомянешь ли ты этот блок?
«Сверху шапка, потом подборка свежих статей, потом форма подписки, потом анонсы конференций» — подборка статей упоминается, значит она часть структуры. А обёртка, которая существует только чтобы навесить display: grid и gap, в описании не всплывёт никогда. Это <div>, и это нормально: <div> не «плохой тег», а честный способ сказать «здесь ничего смыслового нет».
<section aria-labelledby="jobs-title"> <!-- часть структуры страницы -->
<h2 id="jobs-title">Свежие вакансии</h2>
<div class="jobs-grid"> <!-- обёртка только под display: grid -->
<article>...</article>
<article>...</article>
</div>
</section>
Обёртка под сетку тут вложена внутрь секции, а не заменяет её. Смешивать эти две задачи в одном элементе можно, но тогда правка раскладки начинает задевать структуру документа — и наоборот.
Вопрос второй: какая это роль?
Спецификация HTML описывает элементы так (пересказ своими словами, точные формулировки — по ссылкам):
- <article> — законченная, самодостаточная единица контента, которую в принципе можно распространять отдельно: в ленте, в выдаче, в чужом виджете.
- <section> — тематическая часть чего-то большего, обычно с заголовком. Сама по себе она не имеет смысла в отрыве от документа.
Слово «article» тут сбивает с толку, потому что автоматически читается как «статья». В словарном смысле это ещё и «отдельный предмет, единица» — как в выражении «предмет одежды». Предлагаем держать в голове такую аналогию: брюки — это отдельный предмет, их можно надеть с другим комплектом, и они не перестанут быть брюками.
Рабочая проверка: вырежи блок из страницы и представь его отдельно. Карточка поста в ленте, отзыв, комментарий, прогноз погоды в виджете — сохраняют смысл сами по себе, это <article>. Раздел «Характеристики» на странице товара сам по себе бессмысленен: характеристики чего? Это <section>.
Как дать секции имя и превратить её в область
Доступное имя — это то, чем элемент представляется скринридеру. У кнопки это её текст, у картинки — alt, у поля ввода — связанный <label> (и не плейсхолдер). У <section> своего текста нет, поэтому имя приходится указывать явно.
Способ первый: aria-labelledby по видимому заголовку
Предпочтительный вариант. Заголовку раздела даётся id, секция ссылается на него атрибутом aria-labelledby. Значение пишется без решётки — это не CSS-селектор и не якорь, а голый идентификатор.
<section aria-labelledby="digest-title">
<h2 id="digest-title">Дайджест недели</h2>
<article>
<header>
<h3>Как перестать бояться каскадных слоёв</h3>
<p>Материал вышел во вторник</p>
</header>
<p>Короткий тизер материала на пару предложений...</p>
<a href="/posts/cascade-layers">Читать дальше</a>
</article>
<!-- остальные карточки дайджеста -->
</section>
Скринридер прочитает это как «Дайджест недели, область». Главное достоинство способа — имя берётся из того же текста, который видит зрячий пользователь. Заголовок поменяли — имя области поменялось само, рассинхрона не будет.
Способ второй: aria-label, когда подписывать нечем
Если видимого заголовка у блока нет и добавлять его дизайн не позволяет, имя пишется прямо в атрибуте.
<section aria-label="Подборка похожих товаров">
<article>...</article>
<article>...</article>
</section>
Два нюанса. Первый: текст пишется обычным предложением, а не капсом — часть скринридеров читает слова в верхнем регистре по буквам, и «ПОХОЖИЕ ТОВАРЫ» превращается в «пэ-о-ха-о...». Второй: у зрячего пользователя этого текста нет, то есть два человека получают разное описание одного блока. Поэтому aria-label — запасной вариант, а не основной.
Есть и третий путь между ними: добавить настоящий заголовок и спрятать его визуально — тем же приёмом, каким прячут содержимое для скринридеров в клавиатурной вёрстке. Тогда заголовок остаётся в структуре документа, работает и как цель для aria-labelledby, и как ориентир при навигации по заголовкам.
Не превращай в область каждый блок
Соблазн после этого раздела — развесить aria-labelledby на всё подряд. Скотт О'Хара формулирует контраргумент прямо: если ориентир на странице всё, то ориентиров нет вообще. Список областей нужен, чтобы быстро прыгнуть в важное место; когда в нём пятнадцать пунктов, он перестаёт быть быстрым.
Практическое правило: областью делаем то, куда пользователь захочет попасть напрямую — лента, результаты поиска, фильтры, блок с ценами. Внутренние подразделы длинного текста в этом не нуждаются: по ним ходят навигацией по заголовкам, и она уже работает.
Разбираем три типовых макета
Главная страница блога
Каркас страницы обычно очевиден: шапка с навигацией, несколько тематических блоков в основной области, подвал. Тематические блоки — это как раз <section>: они часть главной и по отдельности не живут.
<header>
<nav><!-- навигация по сайту --></nav>
</header>
<main>
<section aria-labelledby="editors-title">
<h2 id="editors-title">Выбор редакции</h2>
<!-- карточки статей, каждая — article -->
</section>
<div class="subscribe-box">
<!-- форма подписки: обёртка нужна только для раскладки -->
</div>
<section aria-labelledby="latest-title">
<h2 id="latest-title">Свежие материалы</h2>
<!-- карточки статей -->
</section>
</main>
<footer>
<nav><!-- навигация по темам --></nav>
</footer>
Форма подписки здесь намеренно осталась <div> — это вставка, а не часть содержательной структуры блога. Решение спорное: если подписка — ключевая цель страницы, ей место в <section> с именем, чтобы до неё можно было допрыгнуть напрямую.
Страница отдельного материала
Весь текст материала — законченная единица, которую можно отдать в ленту или в агрегатор. Значит, снаружи <article>, а крупные разделы внутри — <section>.
<main>
<article>
<h1>Как выбрать между article и section</h1>
<section>
<h2>Что попадает в дерево доступности</h2>
<!-- абзацы -->
</section>
<section>
<h2>Два вопроса вместо спора</h2>
<!-- абзацы -->
<h3>Группировка что-то значит?</h3>
<h3>Какая это роль?</h3>
</section>
<footer>
<p>Материал обновлён после правок в спецификации</p>
</footer>
</article>
</main>
Про <main> тут два ограничения, о которых легко забыть. Он не может лежать внутри <article>, <aside>, <header>, <footer> или <nav> — только снаружи, на уровне <body> или обычной обёртки-<div>. И видимый <main> на странице может быть только один: остальные обязаны нести атрибут hidden.
Лента в веб-приложении
Классический случай, где оба тега встречаются рядом и разница видна лучше всего. Контейнер ленты — часть приложения, отдельно он никуда не уедет: <section>. А каждая запись в ленте самодостаточна и распространяется отдельно — ссылкой, репостом, встраиванием: <article>.
<section aria-labelledby="feed-title">
<h1 id="feed-title">Ваша лента</h1>
<article>
<header>
<p>Марина, вчера в 19:04</p>
</header>
<p>Текст записи...</p>
<article>
<header><p>Ответ от Дениса</p></header>
<p>Текст комментария...</p>
</article>
</article>
</section>
Комментарий внутри записи — тоже <article>, и это не ошибка вложенности. Спецификация ARIA прямо описывает такой сценарий: вложенные элементы образуют обсуждение, где дочерние блоки относятся к содержимому родительского, и вспомогательные технологии в теории могут использовать эту иерархию, чтобы читатель не терял нить дискуссии. На практике реализация этой возможности у скринридеров пока необязательная, но структура от этого хуже не становится.
Вложенность: что во что можно
Ограничений почти нет, и перебрать с уровнями вложенности проще, чем не добрать. Работающие сочетания:
- <article> внутри <section> — карточки внутри тематического блока;
- <section> внутри <article> — разделы длинного материала;
- <article> внутри <article> — ответы, комментарии, цитируемые записи;
- <header> и <footer> внутри любого из них — для метаданных конкретного блока: автор, дата, теги, ссылки на связанные материалы.
<article>
<header>
<h2>Обзор механической клавиатуры</h2>
<p>Автор: Оля К. · Обновлено в марте</p>
</header>
<p>Текст обзора...</p>
<footer>
<p>Похожие обзоры:</p>
<ul>
<li><a href="/reviews/mouse">Мыши для работы</a></li>
<li><a href="/reviews/monitors">Мониторы 27 дюймов</a></li>
</ul>
</footer>
</article>
Важно только, что <header> и <footer> внутри <article> или <section> перестают быть лендмарками страницы: роли banner и contentinfo они получают, только когда лежат на верхнем уровне документа. Внутри секционного элемента это просто шапка и подвал конкретного блока — ровно то, что нужно.
Автоматического аутлайна не существует
Есть живучий миф, из-за которого структуру страниц строят неправильно: будто вложенные <section> сами понижают уровень заголовков, и поэтому в каждой секции можно смело писать <h1> — браузер, мол, посчитает его за <h2> или <h3> по глубине вложенности.
Такой алгоритм действительно был описан в спецификации, но его не реализовал ни один браузер и ни один скринридер. В итоге его убрали из HTML-стандарта и заменили формулировкой, где структура документа выводится просто из уровней <h1>–<h6>, без учёта секционных элементов.
<!-- так писать нельзя: для дерева доступности это три заголовка первого уровня -->
<section>
<h1>Доставка</h1>
<section>
<h1>Сроки</h1>
</section>
</section>
<!-- так правильно: уровень проставлен руками -->
<section>
<h2>Доставка</h2>
<section>
<h3>Сроки</h3>
</section>
</section>
Единственное, что осталось от той истории, — строчка в дефолтных стилях браузеров, из-за которой вложенный <h1> визуально уменьшался. Это легаси, от которого браузеры отказываются: размер шрифта у такого заголовка перестаёт зависеть от вложенности, и страницы, где на это опирались, внезапно получают одинаково огромные заголовки по всему тексту. Плюс браузеры пишут предупреждение в консоль.
Когда section не нужен вовсе
Ещё одна частая ошибка — помнить только про <section> и <article> и заворачивать в первый всё, что кажется важным. Спецификация описывает <section> как самый общий вариант: если под задачу есть более конкретный элемент, берут его.
- Меню и любой набор навигационных ссылок — <nav>.
- Побочный блок рядом с основным контентом: биография автора, врезка, блок «читайте также» — <aside>.
- Основная содержательная область страницы — <main>.
- Блок поиска или фильтрации — <search>.
Последний в списке появился уже после того, как остальные разошлись по проектам, и его часто пропускают. Элемент <search> оборачивает форму поиска или блок фильтров и сразу даёт роль search — ту самую, ради которой раньше писали role="search" руками на <form>.
<!-- было -->
<form role="search" action="/search">
<label for="q">Поиск по сайту</label>
<input id="q" name="q" type="search">
<button type="submit">Найти</button>
</form>
<!-- стало -->
<search>
<form action="/search">
<label for="q">Поиск по сайту</label>
<input id="q" name="q" type="search">
<button type="submit">Найти</button>
</form>
</search>
Зачем это всё, если визуально ничего не меняется
Резонный вопрос: пользователь разницы не увидит, дизайнер не заметит, тесты не упадут. Аргументов в пользу аккуратности несколько, и только первый из них про доступность.
Режим чтения. Упрощённый вид страницы, который умеют делать все крупные браузеры, строится по разметке: движок ищет секционные элементы, чтобы понять, где основной текст, а где обвязка. Мэнди Майкл разбирала это на примере режима чтения Safari: страница на сплошных <div> рискует потерять в таком режиме куски контента — или не открыться в нём вообще.
<!-- режиму чтения не за что зацепиться -->
<div class="wrapper">
<div class="title">Заголовок материала</div>
<div class="text">Первый абзац...</div>
</div>
<!-- здесь границы основного текста очевидны -->
<main>
<article>
<h1>Заголовок материала</h1>
<p>Первый абзац...</p>
</article>
</main>
Дисциплина при работе с текстом. Чтобы выбрать элемент, приходится сформулировать, чем этот кусок является. Часто на этом шаге выясняется, что абзац стоит не в том разделе или что у блока нет заголовка. А сама разбивка текста на разделы с заголовками повышает читаемость: страницы редко читают подряд, их сканируют, цепляясь глазом за заголовки.
Разметка как сообщение коллеге. Открыть чужой шаблон и по одним именам тегов понять, где карточка, где раздел, а где просто обёртка для сетки, — экономия времени. Этого аргумента достаточно, даже если проект никогда не будет проходить аудит доступности.
Короткий чеклист
- Блок не упоминается в устном описании страницы — это <div>, и в этом нет ничего стыдного.
- Блок сохраняет смысл, если вырезать его со страницы, — <article>.
- Блок — тематическая часть большего целого — <section>, и у него должен быть заголовок.
- Секция без доступного имени равна <div>. Нужен ориентир — aria-labelledby на видимый заголовок; нет заголовка — aria-label обычным предложением.
- Ориентиров должно быть немного: только те места, куда пользователь захочет попасть напрямую.
- Уровни заголовков расставляются руками. Вложенность секций на них не влияет.
- Перед <section> проверь, нет ли более конкретного элемента: <nav>, <aside>, <main>, <search>.
- Роль role="section" руками не пишется — она абстрактная.
Разница между этими тегами не в том, какой из них «правильнее». Она в том, сколько информации о структуре страница отдаёт наружу — тем, кто её не видит, тем, что её обрабатывает, и тем, кто будет её править после тебя. Тот же принцип работает и с остальной разметкой: многие недооценённые HTML-атрибуты дают эффект ровно по этой же причине.
Комментарии (0)