Семантическая разметка — это выбор HTML-тегов по смыслу содержимого, а не по внешнему виду: шапка — header, меню — nav, основное содержимое — main, заголовки — h1–h6 без пропусков уровней, действие — button, переход — a. Из такой разметки браузер строит дерево доступности, по которому скринридер перемещается между областями и заголовками. Проверяют разметку валидатором (Nu Html Checker или html-validate) и деревом доступности в DevTools.
После материала вы сможете
- разметить страницу тегами
header,nav,main,section,article,asideиfooterи объяснить, какие из них становятся ориентирами в дереве доступности - выстроить заголовки
h1–h3без пропусков уровней и объяснить, почему уровень не выбирают по размеру шрифта - выбрать между
aиbutton, дать изображениямalt,widthиheight, связать поля формы сlabel - проверить страницу Nu Html Checker,
html-validateи деревом доступности в DevTools
Что знать заранее
Проверено на
HTML Living Standard (WHATWG, 2026-10-09)html-validate 11.16.2Nu Html Checker 26.10.11Microsoft Edge 155 + Playwright 1.64
Сверено с документацией: 11 октября 2026 г.
Содержание
- Семантика: теги по смыслу, а не по виду
- Каркас документа: doctype, lang, head и body
- Смысловые области: header, nav, main, section, article, aside, footer
- Заголовки: одна h1 и уровни без пропусков
- Ссылка или кнопка: правило выбора
- Изображения: alt и размеры
- Формы: label, input, button
- Проверка: валидатор и дерево доступности
Семантика: теги по смыслу, а не по виду
В программировании семантикой называют смысл кода. MDN приводит простой пример: тег h1 сообщает браузеру, что текст внутри — заголовок верхнего уровня, а span с крупным шрифтом только выглядит как заголовок и никакого смысла не несёт. Внешний вид — задача CSS, а HTML описывает, чем является каждый кусок содержимого.
Ниже — «div-суп»: страница, сверстанная безликими div. В браузере её можно сделать красивой, но для машин здесь нет ни шапки, ни меню, ни главного заголовка, ни кнопки.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Ремонт кофемашин</title>
</head>
<body>
<div class="header">
<img src="/img/logo.svg">
<div class="menu">
<a href="#" onclick="openServices()">Услуги</a>
</div>
</div>
<div class="hero">
<div class="title">Ремонт кофемашин в Алматы</div>
<h3>Диагностика и ремонт для кофеен</h3>
<div class="btn" onclick="sendForm()">Отправить заявку</div>
<input placeholder="Телефон">
</div>
</body>
</html>Кто читает разметку, кроме людей? Браузер по HTML строит DOM — дерево элементов, а из него дерево доступности: у каждого узла есть роль (кнопка, ссылка, заголовок), имя и состояние. Скринридеры и другие вспомогательные технологии работают именно с этим деревом: перескакивают между областями страницы и заголовками, объявляют «кнопка» или «ссылка». Режим чтения в браузере, по MDN, ищет main, заголовки и разделы. Поисковые системы MDN тоже называет среди тех, кому смысловая разметка помогает понять содержимое. А разработчику проще найти нужный блок среди nav и footer, чем среди десятка вложенных div.
Div-суп
- Скринридер видит набор текстов и одну ссылку без структуры
- «Кнопка» из
divне получает фокус с клавиатуры - Заголовок первого уровня отсутствует — непонятно, о чём страница
- Каждое поведение приходится дописывать на JavaScript
Семантическая разметка
- Шапка, меню, основное содержимое и подвал — отдельные области
buttonдоступна по Tab и срабатывает по Enter и пробелу- Заголовки
h1–h3образуют оглавление страницы - Браузер даёт поведение бесплатно: фокус, отправка формы, переход
Каркас документа: doctype, lang, head и body
Любая страница начинается с одного и того же каркаса. Он короткий, но каждая строка в нём что-то включает или выключает в браузере.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ремонт кофемашин в Алматы — сервис «Зерно»</title>
<meta name="description" content="Диагностика и ремонт кофемашин для кофеен и офисов в Алматы.">
</head>
<body>
<h1>Ремонт кофемашин в Алматы</h1>
</body>
</html><!DOCTYPE html>
- Зачем нужна
- Включает стандартный режим отображения. Без неё браузер переходит в режим совместимости со старыми сайтами (quirks mode) и считает часть CSS по-старому
<html lang="ru">
- Зачем нужна
- Язык страницы. По нему скринридер выбирает произношение; WCAG 3.1.1 требует, чтобы язык страницы определялся программно
<meta charset="utf-8">
- Зачем нужна
- Кодировка документа — без неё кириллица может превратиться в «кракозябры»
<meta name="viewport" …>
- Зачем нужна
- Без неё мобильный браузер рисует страницу в виртуальном окне шире экрана и уменьшает её целиком
<title>
- Зачем нужна
- Название во вкладке и закладках; поисковые системы используют его как один из сигналов
<meta name="description">
- Зачем нужна
- Краткое описание содержимого страницы
<body>
- Зачем нужна
- Всё, что видит и слышит пользователь
Смысловые области: header, nav, main, section, article, aside, footer
Крупные части страницы размечают тегами-областями. Часть из них становится ориентирами (landmarks) в дереве доступности: скринридер может показать их списком и перейти к нужной одним действием.
header
- Что означает
- Вводная часть: логотип, меню, поиск. Внутри
articleилиsection— шапка этого блока - Роль в дереве доступности
banner, еслиheaderне вложен вmain,article,section,asideилиnav
nav
- Что означает
- Основной блок навигационных ссылок. Не каждая группа ссылок —
nav - Роль в дереве доступности
navigation
main
- Что означает
- Главное содержимое страницы — то, что не повторяется на других страницах
- Роль в дереве доступности
main; видимыйmainв документе только один
section
- Что означает
- Тематический раздел с заголовком, для которого нет более точного тега
- Роль в дереве доступности
region— только если у раздела есть доступное имя, например черезaria-labelledby
article
- Что означает
- Самостоятельный фрагмент: запись блога, комментарий, карточка товара
- Роль в дереве доступности
article
aside
- Что означает
- Косвенно связанное содержимое: врезка, похожие материалы
- Роль в дереве доступности
complementary
footer
- Что означает
- Подвал страницы или блока: контакты, авторство, копирайт
- Роль в дереве доступности
contentinfoдля подвала страницы
div при этом никуда не исчезает. Стандарт HTML прямо говорит: section — не универсальный контейнер, а для обёрток ради стилей и скриптов нужен div. Простая проверка: если у блока есть свой заголовок и его логично увидеть в оглавлении — это section; если блок нужен только для сетки или фона — div. Разницу между section и article стандарт формулирует так: раздел — часть чего-то большего, статья — самостоятельная вещь.
Соберём лендинг сервиса по ремонту кофемашин: шапка с меню, первый экран, услуги, преимущества, форма заявки и подвал. Компания вымышленная, адрес и почта — заглушки.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ремонт кофемашин в Алматы — сервис «Зерно»</title>
<meta name="description" content="Диагностика и ремонт кофемашин для кофеен и офисов в Алматы.">
</head>
<body>
<a href="#main">Перейти к содержимому</a>
<header>
<a href="/"><img src="/img/logo.svg" alt="Зерно — на главную" width="120" height="32"></a>
<nav aria-label="Основное меню">
<ul>
<li><a href="#services">Услуги</a></li>
<li><a href="#benefits">Почему мы</a></li>
<li><a href="#request">Заявка</a></li>
</ul>
</nav>
</header>
<main id="main">
<section>
<h1>Ремонт кофемашин в Алматы</h1>
<p>Диагностика, чистка и ремонт рожковых и автоматических кофемашин для кофеен и офисов.</p>
<a href="#request">Оставить заявку</a>
<img src="/img/master.jpg" alt="Мастер меняет уплотнитель варочной группы рожковой кофемашины" width="1200" height="800">
</section>
<section id="services">
<h2>Услуги</h2>
<article>
<h3>Диагностика</h3>
<p>Находим причину поломки и согласуем стоимость до начала работ.</p>
</article>
<article>
<h3>Чистка от накипи</h3>
<p>Промываем гидросистему и меняем фильтры по регламенту производителя.</p>
</article>
<article>
<h3>Ремонт</h3>
<p>Меняем помпы, уплотнители, датчики и платы управления.</p>
</article>
</section>
<section id="benefits">
<h2>Почему нам доверяют кофейни</h2>
<ul>
<li>Выезд мастера по городу</li>
<li>Оригинальные запчасти</li>
<li>Гарантия на работы</li>
</ul>
</section>
<section id="request">
<h2>Заявка на ремонт</h2>
<form action="/api/request" method="post">
<label for="name">Имя</label>
<input id="name" name="name" type="text" autocomplete="name" required>
<label for="phone">Телефон</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" required>
<label for="problem">Что случилось с кофемашиной</label>
<textarea id="problem" name="problem" rows="4"></textarea>
<button type="submit">Отправить заявку</button>
</form>
</section>
</main>
<footer>
<address>
Алматы, ул. Примерная, 1 · <a href="mailto:hello@example.com">hello@example.com</a>
</address>
<small>© 2026 Сервис «Зерно»</small>
</footer>
</body>
</html>- Первая ссылка «Перейти к содержимому» ведёт к
main id="main": пользователь клавиатуры пропускает меню одним нажатием Enter. Мы проверили — это первый элемент, на который попадает Tab. - У
navестьaria-label: если меню на странице несколько (шапка и подвал), скринридер различает их по имени. - Карточки услуг —
article: каждая самостоятельна и могла бы жить на отдельной странице. Преимущества — простой списокul. - Логотип внутри ссылки получил
altо том, куда ведёт ссылка, а не «логотип». addressв подвале — контактные данные владельца страницы.
Заголовки: одна h1 и уровни без пропусков
Теги h1–h6 задают шесть уровней заголовков. Из всех заголовков страницы по порядку складывается её оглавление (outline), и многие пользователи скринридеров изучают страницу именно так — перескакивая от заголовка к заголовку. Поэтому уровни выбирают по структуре, а не по размеру шрифта: размер — дело CSS.
- Стандарт HTML: каждый следующий заголовок может быть того же уровня, выше или ровно на один уровень глубже предыдущего.
h1→h3безh2— ошибка, а не стиль. - Если на странице есть заголовки, хотя бы один должен быть уровня 1.
- MDN рекомендует одну
h1на страницу — она описывает содержимое так же, какtitle. Несколькоh1стандарт допускает, но это не лучшая практика. - Старый совет «в каждой
sectionсвояh1» устарел: алгоритм, который пересчитывал такие уровни, браузеры так и не реализовали, а теперь вложенныеh1в разделах стандарт считает ошибкой.
1
- Заголовок
- Ремонт кофемашин в Алматы
2
- Заголовок
- Услуги
3
- Заголовок
- Диагностика · Чистка от накипи · Ремонт
2
- Заголовок
- Почему нам доверяют кофейни
2
- Заголовок
- Заявка на ремонт
Ссылка или кнопка: правило выбора
Правило короткое: ссылка a ведёт по адресу — на другую страницу, к якорю, к файлу. Кнопка button выполняет действие на текущей странице — открывает окно, показывает отзывы, отправляет форму. Внешне их можно сделать одинаковыми, но ведут они себя по-разному.
<h1>Ссылки и кнопки</h1>
<!-- Переход на другую страницу или к якорю — ссылка -->
<a href="/prices">Цены на ремонт</a>
<a href="#request">Оставить заявку</a>
<!-- Действие на этой же странице — кнопка с type="button" -->
<button type="button" id="show-reviews">Показать все отзывы</button>
<!-- Отправка формы — кнопка type="submit" внутри form -->
<form action="/api/callback" method="post">
<label for="cb-phone">Телефон</label>
<input id="cb-phone" name="phone" type="tel" autocomplete="tel" required>
<button type="submit">Перезвоните мне</button>
</form><body> из проверенной страницы links-buttons.html.MDN отдельно предупреждает против «ссылок-кнопок» вида href="#" или javascript:void(0) с обработчиком клика: они ломают копирование ссылки, открытие в новой вкладке и закладки, плохо работают, пока JavaScript не загрузился, и сообщают скринридеру неверную роль. Для действия есть button.
Ещё хуже div с onclick. Мы прошли обе страницы клавишей Tab в Edge: на «плохой» фокус попадает на ссылку и поле, а «кнопку» из div пропускает — с клавиатуры её не нажать. В «хорошей» странице button получает фокус и срабатывает и по Enter, и по пробелу. Атрибут role="button" на div этого не исправит: как объясняет web.dev, роль меняет только то, что объявит скринридер, а фокус и клавиши пришлось бы дописывать вручную.
Изображения: alt и размеры
Атрибут alt — текстовая замена изображения. Её прочитает скринридер, её покажет браузер, если картинка не загрузилась. MDN советует описывать смысл изображения, а не сам факт его наличия: не «фото», не имя файла, а то, что важно в контексте. Если изображение чисто декоративное, alt оставляют пустым — тогда вспомогательные технологии его пропускают. Если картинка внутри ссылки или кнопки, alt описывает действие или адрес.
<h1>Изображения</h1>
<!-- Содержательное: alt передаёт то, что важно на снимке -->
<img src="/img/master.jpg" alt="Мастер меняет уплотнитель варочной группы рожковой кофемашины" width="1200" height="800">
<!-- Декоративное: пустой alt — скринридер его пропустит -->
<img src="/img/beans-pattern.svg" alt="" width="400" height="80">
<!-- Картинка-ссылка: alt говорит, куда ведёт ссылка -->
<a href="/"><img src="/img/logo.svg" alt="Зерно — на главную" width="120" height="32"></a><body>; на странице подключено правило CSS img { max-width: 100%; height: auto; }.В дереве доступности этой страницы два изображения, а не три: декоративный узор с alt="" браузер исключил, а ссылка-логотип получила имя «Зерно — на главную» из alt.
Атрибуты width и height — исходные размеры файла в пикселях, без единиц. По ним браузер заранее вычисляет пропорции и резервирует место, поэтому текст не прыгает, когда картинка догрузится. Вместе с CSS max-width: 100%; height: auto изображение остаётся резиновым. Мы замедлили загрузку картинки на 3 секунды и измерили блок до её прихода: на экране шириной 375 px под снимок 1200×800 уже было зарезервировано 375×250 px, а у такой же картинки без атрибутов высота была 0.
Формы: label, input, button
Каждому полю нужна подпись label, связанная с ним в коде. Связать можно двумя способами: явно — атрибут for у подписи равен id у поля, или неявно — поле лежит внутри label. Связь даёт две вещи: скринридер объявляет подпись, когда поле получает фокус, а клик по подписи передаёт фокус полю — у чекбокса область нажатия становится больше.
<h1>Подписка на напоминания о чистке</h1>
<form action="/api/subscribe" method="post">
<!-- Явная связь: for у label = id у поля -->
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<!-- Неявная связь: поле внутри label -->
<label>
<input name="consent" type="checkbox" required>
Согласен получать письма о сервисе
</label>
<button type="submit">Подписаться</button>
</form><body> из проверенной страницы form.html.name— ключ, под которым значение уйдёт на сервер. Поле безnameне отправляется.typeменяет поведение:emailпроверяет формат адреса перед отправкой — в нашей проверке форма сnot-an-emailне ушла на сервер, а с корректным адресом ушла.telна телефонах может вызвать цифровую клавиатуру.autocompleteподсказывает браузеру, какие данные ожидаются (name,email,tel), и он предлагает сохранённые значения.requiredне даёт отправить пустое поле. Это удобство для пользователя, а не защита: сервер всё равно обязан проверять данные сам.- Кнопка отправки —
button type="submit"внутриform. Тогда форма отправляется и кликом, и клавишей Enter в поле.
Какие поля вообще оставлять, где показывать ошибки и как сократить форму, разобрано в материале о формах с точки зрения UX — здесь только разметка.
Проверка: валидатор и дерево доступности
Валидатор W3C
Откройте Nu Html Checker на validator.w3.org/nu, вставьте код или адрес страницы. Он найдёт синтаксические ошибки и нарушения стандарта: картинку без
alt, неверную вложенность.html-validate в проекте
Тот же класс проверок локально и в CI:
npm install --save-dev html-validate, файл настроек и запуск черезnpx. Пресетdocumentдобавляет проверку уровней заголовков.Дерево доступности в DevTools
В Chrome DevTools: Elements → вкладка Accessibility → Show accessibility tree. Вместо DOM появится дерево ролей и имён — сразу видно, где области, заголовки и подписи полей.
Клавиатура
Пройдите страницу клавишей Tab. Документация Chrome прямо говорит, что это DevTools за вас не проверит: фокус должен идти по порядку и попадать на всё интерактивное.
{
"extends": ["html-validate:recommended", "html-validate:document"]
}$ npx html-validate bad.html
…\bad.html
9:6 error <img> is missing required "alt" attribute wcag/h37
16:6 error Initial heading level must be <h1> but got <h3> heading-level
18:6 error <input> is missing recommended "type" attribute no-implicit-input-type
18:6 error <input> element does not have a <label> input-missing-label
✖ 4 problems (4 errors, 0 warnings)
More information:
https://html-validate.org/rules/wcag/h37.html
https://html-validate.org/rules/heading-level.html
https://html-validate.org/rules/no-implicit-input-type.html
https://html-validate.org/rules/input-missing-label.html
$ echo $?
1
$ npx html-validate index.html
$ echo $?
0Обратите внимание, чего валидатор не нашёл: div с onclick и ссылку href="#". С точки зрения синтаксиса они корректны — их ловят только дерево доступности и проверка с клавиатуры. Вот как выглядит дерево нашего лендинга — мы сняли его в Edge через ariaSnapshot() из Playwright; в DevTools вы увидите те же роли и имена:
- link "Перейти к содержимому":
- /url: "#main"
- banner:
- link "Зерно — на главную":
- /url: /
- img "Зерно — на главную"
- navigation "Основное меню":
- list:
- listitem:
- link "Услуги":
- /url: "#services"
- listitem:
- link "Почему мы":
- /url: "#benefits"
- listitem:
- link "Заявка":
- /url: "#request"
- main:
- heading "Ремонт кофемашин в Алматы" [level=1]
- paragraph: Диагностика, чистка и ремонт рожковых и автоматических кофемашин для кофеен и офисов.
- link "Оставить заявку":
- /url: "#request"
- img "Мастер меняет уплотнитель варочной группы рожковой кофемашины"
- heading "Услуги" [level=2]
- article:
- heading "Диагностика" [level=3]
- paragraph: Находим причину поломки и согласуем стоимость до начала работ.
- article:
- heading "Чистка от накипи" [level=3]
- paragraph: Промываем гидросистему и меняем фильтры по регламенту производителя.
- article:
- heading "Ремонт" [level=3]
- paragraph: Меняем помпы, уплотнители, датчики и платы управления.
- heading "Почему нам доверяют кофейни" [level=2]
- list:
- listitem: Выезд мастера по городу
- listitem: Оригинальные запчасти
- listitem: Гарантия на работы
- heading "Заявка на ремонт" [level=2]
- text: Имя
- textbox "Имя"
- text: Телефон
- textbox "Телефон"
- text: Что случилось с кофемашиной
- textbox "Что случилось с кофемашиной"
- button "Отправить заявку"
- contentinfo:
- text: Алматы, ул. Примерная, 1 ·
- link "hello@example.com":
- /url: mailto:hello@example.com
- text: © 2026 Сервис «Зерно»- Есть
<!DOCTYPE html>,langуhtml,charset,viewportи осмысленныйtitle - Шапка, меню, основное содержимое и подвал размечены
header,nav,main,footer - Одна
h1, уровни заголовков идут без пропусков - Переходы — ссылки с настоящим адресом, действия —
buttonс нужнымtype - У содержательных картинок осмысленный
alt, у декоративных — пустой; заданыwidthиheight - У каждого поля есть связанный
label, у полей —nameи подходящийtype - Валидатор без ошибок, Tab проходит по всем интерактивным элементам
Следующий шаг — раскладка: как расставить эти блоки по сетке с помощью Flexbox и Grid, не трогая разметку. Подробнее о доступности в коде — фокусе, ARIA и проверке скринридером — в отдельном уроке направления.
Как проверено3 · пройденосверено 11 октября 2026 г.
- запущено, работает
skeleton.html, index.html, links-buttons.html, images.html, form.html, bad-fixed.html (RU и EN)
html-validate 11.16.2 (пресеты recommended + document) — 0 ошибок; Nu Html Checker 26.10.11 (validator.w3.org/nu/?out=json) — messages: []
- запущено, работает
bad.html + вывод html-validate
реальный вывод html-validate: 4 ошибки (wcag/h37, heading-level, no-implicit-input-type, input-missing-label), код выхода 1; Nu Html Checker — ошибка alt и предупреждение об отсутствии h1
- запущено, работает
поведение в браузере (browser-check.mjs, fragments-check.mjs, fixed-check.mjs)
Microsoft Edge 155 через Playwright 1.64 (channel msedge), RU и EN: landmarks banner/navigation/main/contentinfo, section без имени не region, с aria-labelledby — region; заголовки H1 H2 H3 H3 H3 H2 H2; клик по label фокусирует поле; первый Tab — skip-link; div с onclick не получает фокус; button по Enter и Space; button без type в форме = submit; alt="" исключён из дерева; type=email блокирует отправку; width/height резервируют 375×250 до загрузки (без них высота 0); нет ошибок в консоли; aria-snapshot.yml в статье — реальный вывод ariaSnapshot()
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.