Ориентиры, заголовки и skip-link
Зрячий человек находит нужный блок за долю секунды. Разбираемся, как дать ту же скорость тому, кто страницу не видит.
Ориентир (landmark) — крупная именованная область страницы: шапка, навигация, основное содержимое, сайдбар, подвал. Скринридер умеет строить из ориентиров список и прыгать между ними одной клавишей — это аналог того, как вы «схватываете» структуру страницы глазами.
Зачем это на практике
Откройте любой интернет-магазин. Шапка, меню на 30 пунктов, баннер, фильтры, товары. Вы смотрите на страницу и мгновенно перескакиваете взглядом к товарам, игнорируя всё остальное. Человек со скринридером слышит страницу линейно, сверху вниз: логотип, поиск, 30 пунктов меню, баннер, фильтры… и только потом товары. Если бы навигация была возможна только так, каждый переход по сайту означал бы полторы минуты прослушивания одного и того же меню.
Поэтому скринридеры дают три способа быстрой навигации, и все три вы включаете разметкой:
- по ориентирам — в NVDA клавиша
D, в VoiceOver — ротор, раздел «Ориентиры»; - по заголовкам — клавиша
H(и1–6для конкретного уровня); - по ссылкам, кнопкам, полям формы — отдельные списки.
Именно эти списки и есть настоящая карта сайта для незрячего. Если на странице сплошные <div> — все три списка пусты, и остаётся единственный способ: слушать всё подряд.
Ориентиры: разметка и именование
Хорошая новость: ориентиры почти целиком берутся из тегов, которые мы разобрали в первом уроке.
<body>
<a href="#content" class="skip-link">Перейти к содержимому</a>
<header> <!-- ориентир banner -->
<a href="/">CodeChick</a>
<nav aria-label="Основное меню"> <!-- ориентир navigation -->
<ul>
<li><a href="/courses">Курсы</a></li>
<li><a href="/challenges">Задачи</a></li>
</ul>
</nav>
<form role="search" action="/search"> <!-- ориентир search -->
<label for="q">Поиск по сайту</label>
<input id="q" name="q" type="search">
<button type="submit">Найти</button>
</form>
</header>
<main id="content" tabindex="-1"> <!-- ориентир main -->
<h1>Каталог курсов</h1>
<article>
<h2>Python с нуля</h2>
<p>24 урока, 60 задач.</p>
</article>
</main>
<aside aria-label="Похожие курсы"> <!-- ориентир complementary -->
<h2>Смотрите также</h2>
</aside>
<footer> <!-- ориентир contentinfo -->
<nav aria-label="Документы">
<a href="/terms">Условия</a>
</nav>
</footer>
</body>
Список ориентиров, который получит пользователь, выглядит так:
Результат:
Ориентиры (6):
1. баннер
2. навигация «Основное меню»
3. поиск
4. основное
5. дополнительное «Похожие курсы»
6. нижний колонтитул
навигация «Документы»
Обратите внимание на aria-label у обоих <nav>. Навигаций на странице обычно несколько (главное меню, хлебные крошки, подвал), и без имён пользователь услышит «навигация, навигация, навигация» — выбирать наугад. Правило: если ориентир такого типа на странице не один, дайте каждому имя. И не пишите в aria-label слово «навигация» — скринридер добавит роль сам, получится «навигация Навигация по сайту».
Второе правило: весь значимый контент должен лежать внутри какого-нибудь ориентира. Текст, оказавшийся между </header> и <main>, при навигации по ориентирам будет попросту пропущен.
Заголовки как второй слой навигации
Ориентиры дают крупные области, заголовки — содержание внутри них. Список заголовков — это оглавление, и оно должно читаться осмысленно даже в отрыве от страницы:
Результат:
Заголовки:
1 Каталог курсов
2 Python с нуля
2 HTML и CSS
2 Смотрите также
Если какой-то важный блок визуально «понятен без подписи» (например, список фильтров слева), для скринридера он всё равно безымянный. Здесь помогает визуально скрытый заголовок — он есть в разметке и в дереве доступности, но не виден на экране:
<section aria-labelledby="filters-title">
<h2 id="filters-title" class="visually-hidden">Фильтры каталога</h2>
<!-- чекбоксы фильтров -->
</section>
/* прячем от глаз, но НЕ от скринридера */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Важно: display: none и visibility: hidden здесь не подходят — они убирают элемент и из дерева доступности тоже. Скрывать «только визуально» нужно именно так, через вынос за пределы видимой области.
Skip-link: ссылка «Перейти к содержимому»
Пользователь клавиатуры не может «перепрыгнуть взглядом» через меню — он жмёт Tab и проходит все 30 пунктов. На каждой странице. Skip-link — это первая в порядке табуляции ссылка, которая переносит фокус сразу в <main>.
<body>
<a href="#content" class="skip-link">Перейти к содержимому</a>
<header>...</header>
<main id="content" tabindex="-1">
<h1>Каталог курсов</h1>
</main>
</body>
.skip-link {
position: absolute;
top: -100px; /* спрятана над экраном */
left: 1rem;
z-index: 100;
padding: 0.75rem 1rem;
background: #fff;
color: #111;
border: 2px solid #111;
}
.skip-link:focus {
top: 1rem; /* появляется при фокусе с клавиатуры */
}
Три детали, которые чаще всего забывают:
- Ссылка должна быть первой в DOM — иначе до неё придётся дотабаться через полшапки, и смысл теряется.
- Она обязана становиться видимой при фокусе. Невидимая skip-link — ловушка для зрячего клавиатурного пользователя: фокус уехал непонятно куда.
tabindex="-1"на<main>. Без него часть браузеров прокручивает страницу к якорю, но фокус оставляет в начале документа — и следующийTabвернёт человека обратно в меню. Атрибутtabindex="-1"делает элемент фокусируемым программно, не добавляя его в порядок табуляции.
Порядок чтения: его задаёт DOM, а не CSS
Скринридер и клавиатура идут по порядку в разметке. Глаз идёт по порядку на экране. Обычно они совпадают — но CSS умеет их развести, и тогда интерфейс разваливается.
/* опасно: визуально блоки поменялись местами, в DOM — нет */
.sidebar { order: -1; } /* сайдбар нарисован слева, но читается последним */
.row { flex-direction: row-reverse; }
Получается разрыв: человек видит сайдбар первым, а Tab уводит его туда в самом конце. Для незрячего порядок вообще другой. Спецификация WCAG называет это нарушением критерия «Значимая последовательность»: если визуальный порядок важен для понимания, он должен совпадать с порядком в DOM. Меняйте местами блоки в разметке, а не переставляйте их визуально.
Из той же оперы — положительный tabindex:
<!-- почти всегда ошибка: ломает естественный порядок обхода всей страницы -->
<input tabindex="3">
<input tabindex="1">
Допустимых значений практически два: 0 (обычный порядок) и -1 (фокус только из кода). Всё остальное — попытка руками управлять тем, что и так работает правильно, если разметка в верном порядке.
Как это работает
Ориентиры — это те же роли из дерева доступности, только «крупные»: banner, navigation, main, complementary, contentinfo, search, region, form. Скринридер обходит дерево, собирает все узлы с такими ролями и складывает в отдельный список — тот самый, что открывается по D или через ротор. Имя ориентира берётся из aria-label, из aria-labelledby или из заголовка внутри. Точно так же собирается список заголовков — по узлам с ролью heading и их уровням.
Одна тонкость: <header> и <footer> становятся ориентирами banner и contentinfo, только если они не вложены в <article>, <section> или <main>. Подвал внутри карточки товара — просто подвал карточки, и это правильно: иначе на странице оказалось бы двадцать «нижних колонтитулов».
Проверить всё это можно без скринридера. В Chrome DevTools включите Elements → Accessibility → Full-page accessibility tree — увидите ровно тот список, что получит пользователь. Расширения вроде Landmarks или Accessibility Insights рисуют ориентиры прямо поверх страницы.
Частые ошибки
| Ошибка | Последствие |
Несколько <nav> без aria-label | «навигация, навигация, навигация» — не выбрать нужную |
| Контент вне ориентиров | Блок выпадает из навигации по landmarks |
Skip-link спрятана через display: none | Ссылка не фокусируется — её просто нет |
Skip-link не показывается при :focus | Зрячий клавиатурный пользователь теряет фокус |
Нет tabindex="-1" на цели skip-link | Страница прокрутилась, но фокус остался в шапке |
Визуальный порядок задан order / row-reverse | Порядок табуляции не совпадает с картинкой |
Положительный tabindex | Хаотичный обход всей страницы |
Пустой <section> без имени | Ориентира не создаёт — только видимость семантики |
Итоги
- Скринридер навигирует тремя списками: ориентиры, заголовки, ссылки. Все три вы наполняете разметкой.
header,nav,main,aside,footer,form role="search"— готовые ориентиры без единой строчки ARIA.- Одинаковых ориентиров несколько — именуйте через
aria-label, без слова «навигация» в тексте. - Весь значимый контент — внутри ориентиров; ничего не должно «висеть» между ними.
- Skip-link: первая ссылка в DOM, видимая при фокусе, цель —
<main id="content" tabindex="-1">. - Скрывать «только визуально» — классом
.visually-hidden, а неdisplay: none. - Порядок чтения задаёт DOM. CSS-перестановки (
order,row-reverse) и положительныйtabindexего ломают.