Семантические теги вместо div

Разбираемся, почему скринридер «не видит» аккуратно свёрстанный div и что меняет один-единственный правильный тег.

Семантический тег — тег, который сообщает браузеру и вспомогательным технологиям смысл содержимого, а не только его вид. <button> говорит «я кнопка», <nav> — «я навигация», <h2> — «я заголовок раздела». <div> не говорит ничего: это пустой контейнер, у которого есть внешность, но нет роли.

Зачем это нужно на практике

Типичная история. Дизайнер отдал макет, вёрстка сделана пиксель в пиксель, кнопка «Подписаться» выглядит идеально — синяя, со скруглениями, с ховером. Через месяц приходит письмо от пользователя: «Я не могу подписаться на вашу рассылку». Открываете страницу, всё работает. Отключаете мышь — и оказывается, что до кнопки просто нельзя добраться клавишей Tab. Потому что это не кнопка. Это <div class="btn"> с обработчиком клика.

Браузер строит из вашего HTML две вещи. Первую вы видите — это DOM и картинка на экране. Вторую вы не видите, но именно ей пользуется скринридер, — это дерево доступности (accessibility tree). В нём у каждого элемента есть три характеристики: роль (что это такое), имя (как это назвать вслух) и состояние (нажато, развёрнуто, отключено). Семантический тег заполняет всё это автоматически и бесплатно. <div> оставляет пустоту: роль «generic», имени нет, состояний нет. Красивый прямоугольник, который для незрячего пользователя просто не существует.

Каркас страницы: плохо и хорошо

Начнём с самого частого антипаттерна — «супчик из дивов» (div soup). Такую разметку выдаёт почти любой конвертер макета в код.

Плохо:

<div class="page">
  <div class="top">
    <div class="logo">CodeChick</div>
    <div class="menu">
      <div class="menu-item">Курсы</div>
      <div class="menu-item">Задачи</div>
    </div>
  </div>

  <div class="content">
    <div class="big-text">Веб-доступность</div>
    <div class="text">Сайты, которыми можно пользоваться без мыши и без зрения.</div>
    <div class="btn" onclick="subscribe()">Подписаться</div>
  </div>

  <div class="bottom">© 2026 CodeChick</div>
</div>

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

Хорошо:

<body>
  <header>
    <a href="/" class="logo">CodeChick</a>
    <nav aria-label="Основное меню">
      <ul>
        <li><a href="/courses">Курсы</a></li>
        <li><a href="/challenges">Задачи</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>Веб-доступность</h1>
    <p>Сайты, которыми можно пользоваться без мыши и без зрения.</p>
    <button type="button" class="btn">Подписаться</button>
  </main>

  <footer>
    <p>© 2026 CodeChick</p>
  </footer>
</body>

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

Результат:

баннер
  ссылка «CodeChick»
  навигация «Основное меню»
    список из 2 элементов
      ссылка «Курсы»
      ссылка «Задачи»
основное
  заголовок 1-го уровня: Веб-доступность
  Сайты, которыми можно пользоваться без мыши и без зрения.
  кнопка «Подписаться»
нижний колонтитул
  © 2026 CodeChick

Это уже не текстовое полотно, а карта, по которой можно прыгать. Никакого ARIA мы не писали — всё дали сами теги.

Мини-справочник каркасных тегов

ТегРоль в дереве доступностиКогда применять
<header>bannerШапка сайта: логотип, поиск, меню. Один на странице (если это прямой потомок body)
<nav>navigationБлок ссылок навигации. Их может быть несколько — тогда именуйте через aria-label
<main>mainУникальное содержимое страницы. Строго один на страницу
<article>articleСамодостаточный кусок: статья, карточка товара, комментарий
<section>region (только с именем)Тематический раздел. Без aria-label или заголовка внутри — почти бесполезен
<aside>complementaryСопутствующее: сайдбар, «читайте также»
<footer>contentinfoПодвал сайта: копирайт, контакты

Обратите внимание на подвох с <section>: многие ставят его вместо <div> «для красоты». Но безымянная секция не создаёт ориентира — она просто <div> с более длинным именем. Хотите ориентир — дайте имя: <section aria-label="Отзывы">.

Заголовки — это оглавление, а не размер шрифта

Второй по частоте грех — выбирать уровень заголовка по кеглю. «Тут надо помельче, поставлю h4». Для скринридера заголовки — это оглавление книги: по ним прыгают клавишей H, из них строят список всех разделов страницы. Пропущенный уровень читается так же, как в книге прыжок с главы сразу на подпункт 1.2.3 — человек теряет ощущение, где он находится.

Плохо — уровни выбраны по внешнему виду:

<h1>Курсы</h1>
<h4>Для начинающих</h4>      <!-- пропущены h2 и h3, просто «шрифт помельче» -->
<h2>Python с нуля</h2>        <!-- а тут вдруг «покрупнее» -->

Хорошо — уровни отражают вложенность, а размер задаёт CSS:

<h1>Курсы</h1>
  <h2>Для начинающих</h2>
    <h3>Python с нуля</h3>
    <h3>HTML и CSS</h3>
  <h2>Для продвинутых</h2>
    <h3>Алгоритмы</h3>
/* нужен мелкий заголовок раздела? это работа CSS, а не тега */
.section-title {
  font-size: 1rem;
  font-weight: 600;
}

Правила простые: один h1 на страницу (это её название), уровни идут вниз без пропусков, заголовок описывает то, что под ним, а не то, как он выглядит.

Почему div с onclick — не кнопка

Самый живучий антипаттерн. Выглядит безобидно:

// плохо: обработчик повесили на div
const fakeButton = document.querySelector('.btn');
fakeButton.addEventListener('click', subscribe);

Мышью работает. А теперь список того, что вы не получили, взяв <div> вместо <button>:

  • Фокус. Элемента нет в порядке табуляции — клавишей Tab до него не дойти вообще.
  • Клавиатура. Enter и Space по нему не срабатывают: у div нет поведения по умолчанию.
  • Роль. Скринридер прочитает текст «Подписаться» как обычный абзац и не скажет «кнопка». Пользователь не поймёт, что тут можно что-то нажать.
  • Состояние. Нет disabled: заблокировать элемент можно только вручную, через классы и проверки в коде.
  • Формы. Внутри <form> такой «кнопкой» нельзя отправить форму — type="submit" у div не существует.
  • Голосовое управление. Команда «нажми Подписаться» в Voice Control не сработает: система ищет элементы с ролью кнопки.

Дальше начинается лечение симптомов. Разработчик добавляет role="button", потом tabindex="0", потом обработчик клавиш:

<!-- костыль: воспроизводим кнопку руками -->
<div class="btn" role="button" tabindex="0">Подписаться</div>
fakeButton.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();   // иначе Space прокрутит страницу
    subscribe();
  }
});

Три атрибута, два обработчика, preventDefault — и всё это ради того, чтобы получить то, что уже встроено в один тег:

<button type="button" class="btn">Подписаться</button>

Отсюда золотое правило доступности, известное как первое правило ARIA: не используйте ARIA, если есть подходящий нативный элемент. Родной тег всегда лучше самодельной имитации — хотя бы потому, что он не сломается при следующем рефакторинге.

Как это работает

Когда браузер парсит HTML, он строит DOM, а параллельно с ним — дерево доступности. В нём остаются только элементы, у которых есть смысл; чисто оформительские обёртки схлопываются. Каждому узлу назначается:

  • рольbutton, link, heading, navigation. Берётся из имени тега (у <div> она generic);
  • имя — текст внутри, alt, <label> или aria-label;
  • состояние и свойстваdisabled, checked, aria-expanded, уровень заголовка.

Скринридер читает это дерево и озвучивает по формуле «имя, роль, состояние»: «Подписаться, кнопка», «Курсы, ссылка», «Веб-доступность, заголовок 1-го уровня». Проверить дерево можно прямо в DevTools: вкладка Elements → Accessibility в Chrome показывает роль и вычисленное имя выбранного элемента. Если там пусто или написано generic — вы только что нашли баг доступности.

Частые ошибки

ОшибкаЧем плохоКак надо
<div onclick> вместо кнопкиНет фокуса, клавиатуры и роли<button type="button">
Уровень заголовка выбран по размеру шрифтаЛомается оглавление страницыУровень — по вложенности, размер — в CSS
Несколько <main> на страницеСкринридер не знает, где основное содержимоеРовно один <main>
<section> вместо <div> без имениОриентира не создаёт, только шум в разметкеЛибо aria-label, либо обычный <div>
Меню как набор <div class="menu-item">Не объявляется количество пунктов<nav> + <ul> + <a>
«Кнопка» с иконкой без текстаРоль есть, имени нет: «кнопка, пусто»Добавить aria-label или скрытый текст

Итоги

  • Тег — это не про внешность, а про роль. Внешность целиком отдаём CSS.
  • Каркас страницы: headernavmainfooter. Пять минут работы, огромный эффект.
  • Заголовки h1h6 — оглавление страницы: один h1, уровни без пропусков.
  • <div> с обработчиком клика недоступен с клавиатуры и не имеет роли. Нативная кнопка даёт то же самое бесплатно.
  • Первое правило ARIA: если есть подходящий нативный тег — берите его, а не имитацию.
  • Проверяйте себя в DevTools: вкладка Accessibility покажет роль и имя элемента.
Проверьте себя
1. Что произойдёт с элементом <div class="btn" onclick="send()">Отправить</div> при работе с клавиатуры?
AВсё в порядке: браузер сам добавляет div с onclick в порядок табуляции
BДо него нельзя добраться клавишей Tab, и Enter/Space по нему не сработают
CTab работает, но скринридер объявит элемент как ссылку
DОн сработает только по Space, но не по Enter
2. Почему нельзя выбирать уровень заголовка (h2, h3, h4) исходя из нужного размера шрифта?
AПотому что браузер запрещает менять размер заголовков через CSS
BПотому что поисковики учитывают только h1 и h2
CПотому что заголовки формируют оглавление страницы, по которому навигирует скринридер, и пропуск уровня ломает структуру
DПотому что теги ниже h3 не попадают в дерево доступности