Семантические теги вместо 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.
- Каркас страницы:
header→nav→main→footer. Пять минут работы, огромный эффект. - Заголовки
h1–h6— оглавление страницы: одинh1, уровни без пропусков. <div>с обработчиком клика недоступен с клавиатуры и не имеет роли. Нативная кнопка даёт то же самое бесплатно.- Первое правило ARIA: если есть подходящий нативный тег — берите его, а не имитацию.
- Проверяйте себя в DevTools: вкладка Accessibility покажет роль и имя элемента.