Хабр, я уже третий месяц пишу про доступность вместе с Ильёй. Мы показываем, как HTML и CSS могут улучшить или ухудшить её. Напоминаю, что Илья — мой незрячий знакомый, который помогает мне найти наши косяки в вёрстке. Мы уже написали первую и вторую части цикла статей.
Сегодня уже будет не только HTML и CSS. В некоторых кейсах мы будем использовать ARIA-атрибуты. Я расскажу:
- как мы незаметно потеряли пользу элементов
<section>
и<form>
; - как атрибут
tabindex
запутывает незрячего пользователя; - почему визуально скрытые элементы — проблема современных интерфейсов;
- что делать с паттерном «Звёздочка» для обязательных полей.
Давайте начнём!
▍ Польза от элемента <section>
и <form>
потеряна
Стандарт Accessible Rich Internet Applications (WAI-ARIA) относит элементы <section>
и <form>
к группе навигационных элементов, с помощью которой мы можем дать быстрый доступ к основным областям страницы пользователям скринридера. Элемент <section>
используется для доступа к разделам страницам, а элемент <form>
— к формам. Только на практике нас ждёт сюрприз.
Начну с элемента <section>
. Я проинспектирую следующую разметку скринридером JAWS.
<section>
<h2>Обо мне</h2>
<!-- здесь контент раздела -->
</section>
<section>
<h2>Портфолио</h2>
<!-- здесь контент раздела -->
</section>
<section>
<h2>Контакты</h2>
<!-- здесь контент раздела -->
</section>
Он говорит мне: «Области на странице не найдены». Получается, для него элементов <section>
не существует. Погуглив, я понял, что если не указывать текстовую подсказку для элемента, то он будет определён скринридерами как элемент <div>
. Сделать это можно при помощи атрибута aria-labelledby
.
Атрибут помогает скринридеру найти текстовое описание элемента. Другими словами, атрибут создаёт «связь» между элементом, который нужно описать, и элементом, который будет описывать его.
В нашем случае нужно описать элемент <section>
, поэтому добавим к нему атрибут aria-labelledby
, а элемент, который будет описывать его — это заголовок <h2>
. К нему добавим атрибут id
со значением, которое было использовано для атрибута aria-labelledby
.
Например, для раздела «Обо мне» я буду использовать значение about-me-heading
, для раздела «Портфолио» — значение portfolio-heading
, а для раздела «Контакты» — значение contacts-heading
.
<section aria-labelledby="about-me-heading">
<h2 id="about-me-heading">Обо мне</h2>
<!-- здесь контент раздела -->
</section>
<section aria-labelledby="portfolio-heading">
<h2 id="portfolio-heading">Портфолио</h2>
<!-- здесь контент раздела -->
</section>
<section aria-labelledby="contacts-heading">
<h2 id="contacts-heading">Контакты</h2>
<!-- здесь контент раздела -->
</section>
Класс! Теперь JAWS увидел созданные разделы.
Перейдём теперь к формам. Для примера я создам следующую разметку:
<form>
<input type="text">
<button>Подписаться</button>
</form>
Проинспектировав её JAWS, он выдаёт такое же сообщение, как в примере с элементом <section>
: «Области на странице не найдены».
В качестве решения можно также использовать атрибут aria-labelledby
, но тогда нужно добавить заголовок внутрь формы. Но предположим, что мы не можем этого сделать. Здесь будет полезен атрибут aria-label
.
Как в случае с атрибутом aria-label
, он помогает браузеру найти текстовое описание. Только происходит это не через связь с помощью атрибута id
, а просто можно написать текст. Например, «Подписаться на наши новости», а потом добавить его к элементу <form>
:
<form aria-label="Подписаться на наши новости">
<input type="text">
<button>Подписаться</button>
</form>
Вот теперь полный порядок. Скринридер нашёл форму, и пользователь может заполнить её. Кстати, если вы ничего не знаете об атрибуте aria-label
, я написал отдельную статью, в которой детально рассказал о нём.
▍ tabindex
заводит пользователя скринридера в тупик
Когда мы работали над одним проектом с Ильёй, однажды он прислал мне сообщение:
«Стас, не работает последовательная навигация клавишейTab
по пунктам меню сайта. При нажатииTab
из раздела меню „Меры государственной поддержки АПК“, фокус вместо родительского пункта „Министерство“ попадает на ссылку „Войти“, и дальше идёт по шапке, а потом снова в раздел меню „Документы“. При переключении клавишами стрелок такого поведения не наблюдается. Необходимо обеспечить логическую последовательность навигации при использованииTab
».
Я открыл страницу и начал нажимать клавишу Tab
. Первый Tab
меню привёл в пункт «Министерство». Потом дошёл до пункта «Меры государственной поддержки АПК». Жму ещё раз. Попадаю вверх сайта к ссылке «Войти» вместо родительского пункта «Министерство».
Для объяснения причины такого поведения я упростил код шапки страницы до следующего:
<header>
<a href="/auth/">Войти</a>
<!-- другие элементы -->
<ul>
<li>
<a tabindex="1" href="/about/">Министерство</a>
<ul>
<li><a href="/uchetnaya-politika-ministerstva" tabindex="1">Учетная политика министерства</a></li>
<li><a href="/about/reports" tabindex="1">Отчеты</a></li>
<li><a href="/about/meri-gospoderzki-apk" tabindex="1">Меры государственной поддержки АПК</a></li>
</ul>
</li>
<li><a href="/docs/">Документы</a></li>
<li><a href="/contacts/">Контакты</a></li>
</ul>
</header>
Видите, у ссылок объявлено tabindex="1"
? Собака зарыта в этом атрибуте.
По умолчанию браузеры переносят фокус согласно тому порядку, в каком интерактивные элементы находятся в разметке. Но если мы используем положительное значение для атрибута tabindex
, то у элемента, к которому оно применено, появляется приоритет. Это произошло в примере выше.
Разработчики добавили для ссылок в меню tabindex="1"
, поэтому первый Tab
переносил фокус к пункту «Министерство». Далее скринридер идёт по всем ссылкам, у которых также установлен атрибут tabindex
. После пункта «Меры государственной поддержки АПК» идёт уже обычный порядок переноса фокуса, основываясь на порядке элементов. В коде первый интерактивный элемент это ссылка «Войти». Поэтому скринридер переносил Илью на эту ссылку из меню, тем самым запутывая его.
Но это не единственная причина запутывания. Илья также использует клавиши стрелок, чтобы переключаться между элементами. Что это такое? Пользователь скринридера с помощью клавиши ↓
переходит вниз по элементам, а с помощью клавиши ↑
— наверх.
При тестировании примера Илья нажал клавишу ↓
, и первым элементом была ссылка «Войти», а не пункт «Министерство», как при нажатии клавиши Tab
. Это тоже сбило с толку Илью.
«Если при нажатии на клавишиTab
пропускаются какие-то элементы, но они находятся скринридером с помощью клавиш стрелок, то это вызывает у меня недоумение. Сразу ощущение, что сайт кривой. Конечно, при большой необходимости я буду использовать стрелки для взаимодействия. Но у меня всё равно останется ощущение брезгливости. Как будто сайт сделан на коленках в подвале.
У людей разные алгоритмы взаимодействия. На моём месте может быть масса людей, которые не используют стрелки. Они просто пройдут при помощи клавишиTab
по навигации, увидят те элементы, которые скринридер нашёл, а об остальных не узнают».
Пожалуйста, избегайте изменения порядка перехода фокуса. Тогда не будет проблем. Все элементы найдутся всеми доступными способами.
▍ Спрячьте уже визуально скрытые элементы
Одной из распространённых проблем современных интерфейсов являются визуально скрытые элементы, но доступные для клавиатуры и скринридера. В случае взаимодействия при помощи клавиатуры я не могу показать пример, потому что показывать нечего. Визуально ничего не происходит. Я просто жму очень много раз клавишу Tab
.
А вот взаимодействие скринридера с интерфейсом рассмотрим.
Перед тем, как сделать скриншот, я переключался по интерфейсу в скринридере JAWS, нажимая клавиши стрелок. Красная обводка появляется вокруг текущего элемента. А на скриншоте выделен подпункт меню, который визуально не отображается.
Давайте поговорим, как же можно скрыть элементы, чтобы они не были доступны скринридеру и клавиатуре. Первое, что приходит мне на ум, использование CSS-свойства display
и visibility
.
button {
display: none; /* или `visibility: hidden` */
}
В этом случае все кнопки будут недоступны. Ещё одним способом является использование атрибутов aria-hidden
и tabindex
, как я показал в следующем фрагменте кода:
<button type="button" aria-hidden="true" tabindex="-1">Только можно кликнуть</button>
Что лучше использовать? Нет универсального решения. Всё зависит от конкретного случая. Но я постараюсь выдать краткий совет.
Если у вас в результате действия пользователя появляется какой-то блок с интерактивными элементами, то лучше использовать CSS-свойства. Например, анимация появления меню, всплывающие окна и т. п. А HTML-атрибуты можно использовать для сокращения количества итераций или скрытия визуально важных элементов, но бесполезных для пользователя скринридера.
К слову, в примере с меню достаточно использовать display: none
для блока, содержащего все подпункты меню.
▍ Обязательное поле не является «звездой»
Мне очень нравился приём, когда для обозначения обязательного поля символ *
вставляется при помощи свойства content
. Мне казалось это крутым, потому что HTML не захламляется.
<div class="field field_required">
<label class="field__label" for="request_anonymous_requester_email">
Ваш email
</label>
<input type="email" id="request_anonymous_requester_email">
</div>
.field_required .field__label::after {
content: "*";
color: #e31420;
}
К сожалению, мне пришлось отказаться от него. Когда я протестировал мою форму скринридером JAWS, то он для каждого поля добавлял «звёздочка». Представляю удивление незрячего. Вот, что думает Илья о ней:
«Я знаю, что такое звёздочка, только по зрячему опыту. Есть множество людей, которые не понимают её значение. Единственным исключением является случай, когда над полем указывают пояснение, что звёздочка обозначает поля, обязательные для заполнения. Так что это в любом случае не универсальное решение, рассчитанное исключительно на визуальное восприятие».
Поэтому лучше её скрыть. Для этого нужно сначала добавить символ в HTML и скрыть с помощью атрибута aria-hidden
.
<div class="field">
<label class="field__label" for="request_anonymous_requester_email">
Ваш email
<span class="field__required-symbol" aria-hidden="true">*</span>
</label>
<input type="email" id="request_anonymous_requester_email">
</div>
.field__required-symbol {
color: #e31420;
}
А совсем хорошо будет помочь пользователю скринридера узнать, что поле ввода обязательно для заполнения. Тем более, это очень просто сделать. Просто нужно добавить атрибут aria-required
со значением true
.
<div class="field">
<label class="field__label" for="request_anonymous_requester_email">
Ваш email
<span class="field__required-symbol" aria-hidden="true">*</span>
</label>
<input type="email" id="request_anonymous_requester_email" aria-required="true">
</div>
Теперь скринридер JAWS говорит: «Ваш email. Редактор обязательный. Введите текст».
▍ Заключение
С помощью этой статьи мы с Ильёй хотели призвать вас:
- добавлять атрибут
aria-labelledby
иaria-label
для элементов<section>
и<form>
; - не трогать порядок перемещения фокуса;
- не забывать скрывать элементы, которые визуально появляются после взаимодействия пользователя;
- скрывать «Звёздочку» у полей для пользователя скринридера.
Ещё раз оставлю ссылки на первую и вторую части. Также нам будет интересен и ваш опыт. Делитесь своими кейсами в комментариях. Спасибо за чтение.
Узнавайте о новых акциях и промокодах первыми из нашего Telegram-канала