Как делать11 октября 2026 г.10 мин чтения
Вёрстка: HTML и CSSШаг 2 из 7

HTML: семантическая разметка, которую понимают браузеры, поиск и скринридеры

Коротко

Семантическая разметка — это выбор HTML-тегов по смыслу содержимого, а не по внешнему виду: шапка — header, меню — nav, основное содержимое — main, заголовки — h1–h6 без пропусков уровней, действие — button, переход — a. Из такой разметки браузер строит дерево доступности, по которому скринридер перемещается между областями и заголовками. Проверяют разметку валидатором (Nu Html Checker или html-validate) и деревом доступности в DevTools.

ОсновыУровень: НачальныйHTML

После материала вы сможете

  • разметить страницу тегами 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 г.

Содержание
  1. 01Семантика: теги по смыслу, а не по виду
  2. 02Каркас документа: doctype, lang, head и body
  3. 03Смысловые области: header, nav, main, section, article, aside, footer
  4. 04Заголовки: одна h1 и уровни без пропусков
  5. 05Ссылка или кнопка: правило выбора
  6. 06Изображения: alt и размеры
  7. 07Формы: label, input, button
  8. 08Проверка: валидатор и дерево доступности

Семантика: теги по смыслу, а не по виду

В программировании семантикой называют смысл кода. MDN приводит простой пример: тег h1 сообщает браузеру, что текст внутри — заголовок верхнего уровня, а span с крупным шрифтом только выглядит как заголовок и никакого смысла не несёт. Внешний вид — задача CSS, а HTML описывает, чем является каждый кусок содержимого.

Ниже — «div-суп»: страница, сверстанная безликими div. В браузере её можно сделать красивой, но для машин здесь нет ни шапки, ни меню, ни главного заголовка, ни кнопки.

bad.html
<!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

Любая страница начинается с одного и того же каркаса. Он короткий, но каждая строка в нём что-то включает или выключает в браузере.

skeleton.html
<!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>
Минимальный корректный документ: проходит Nu Html Checker и html-validate без замечаний.

<!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 стандарт формулирует так: раздел — часть чего-то большего, статья — самостоятельная вещь.

Соберём лендинг сервиса по ремонту кофемашин: шапка с меню, первый экран, услуги, преимущества, форма заявки и подвал. Компания вымышленная, адрес и почта — заглушки.

index.html
<!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>
Полная страница: 0 замечаний в Nu Html Checker и html-validate, проверена в Microsoft Edge 155.
  • Первая ссылка «Перейти к содержимому» ведёт к 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 выполняет действие на текущей странице — открывает окно, показывает отзывы, отправляет форму. Внешне их можно сделать одинаковыми, но ведут они себя по-разному.

links-buttons.html
<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 описывает действие или адрес.

images.html
<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. Связь даёт две вещи: скринридер объявляет подпись, когда поле получает фокус, а клик по подписи передаёт фокус полю — у чекбокса область нажатия становится больше.

form.html
<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 — здесь только разметка.

Проверка: валидатор и дерево доступности

  1. Валидатор W3C

    Откройте Nu Html Checker на validator.w3.org/nu, вставьте код или адрес страницы. Он найдёт синтаксические ошибки и нарушения стандарта: картинку без alt, неверную вложенность.

  2. html-validate в проекте

    Тот же класс проверок локально и в CI: npm install --save-dev html-validate, файл настроек и запуск через npx. Пресет document добавляет проверку уровней заголовков.

  3. Дерево доступности в DevTools

    В Chrome DevTools: Elements → вкладка Accessibility → Show accessibility tree. Вместо DOM появится дерево ролей и имён — сразу видно, где области, заголовки и подписи полей.

  4. Клавиатура

    Пройдите страницу клавишей Tab. Документация Chrome прямо говорит, что это DevTools за вас не проверит: фокус должен идти по порядку и попадать на всё интерактивное.

.htmlvalidate.json
{
  "extends": ["html-validate:recommended", "html-validate:document"]
}
Настройки html-validate: рекомендуемые правила плюс правила для целых документов.
terminal
$ 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
Реальный вывод html-validate 11.16.2: «плохая» страница — 4 ошибки и код выхода 1, лендинг — без замечаний. Путь к файлу сокращён.

Обратите внимание, чего валидатор не нашёл: div с onclick и ссылку href="#". С точки зрения синтаксиса они корректны — их ловят только дерево доступности и проверка с клавиатуры. Вот как выглядит дерево нашего лендинга — мы сняли его в Edge через ariaSnapshot() из Playwright; в DevTools вы увидите те же роли и имена:

YAML
- 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 Сервис «Зерно»
Дерево доступности лендинга: banner, navigation с именем, main с заголовками 1–3, contentinfo.
  • Есть <!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.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.