Всем привет! Меня зовут Юля Долгун, я фронтенд-разработчик из Поиска. Одна из моих задач — поддерживать доступность в поиске по товарам для пользователей с различными ограничениями здоровья.

Прошлой осенью Яндекс улучшил доступность в ряде своих сервисов. Наша команда тоже принимала участие в этом проекте — исправляла баги доступности и помогала с этим другим разработчикам. Среди всех ошибок некоторые повторялись чаще всего — о них и пойдёт речь в этой статье. Я расскажу, где они скрываются, а также как и почему их нужно исправлять. 

О том, как работают с приложениями незрячие люди, подробно рассказал мой коллега в одной из прошлых статей. Если кратко, существуют программы экранного доступа, которые переводят элементы интерфейса в текст. Затем этот текст озвучивается с помощью синтезатора речи либо выводится на специальный брайлевский дисплей.

Кнопки

Начнём с простого — кнопок. При их разметке я часто встречала такой код:

<div className="button" onClick={doSomething}>открыть фото котиков</div>

Такая разметка плоха тем, что при навигации по странице табами эта кнопка не получает фокус — он проскакивает мимо. То есть, пользователь клавиатуры не сможет нажать эту кнопку, потому что просто не узнает о её существовании. А пользователь скринридера хоть и сможет дойти до кнопки, но ему не анонсируют, что это именно кнопка. Он не поймёт, что если он нажмёт на этот текст, то что-то произойдёт.

Ещё хуже, если внутри кода кнопки не будет никакого текста:

<div className="button" onClick={doSomething} />

Такое можно встретить, если на самой кнопке нет текста, а только картинка или иконка. С такой разметкой пользователь скринридера даже не узнает о том, что в этом месте есть кнопка — программа ему ничего не зачитает.

Исправить ситуацию просто — используйте тег button:

<button class="button" onClick={doSomething}>открыть фото котиков</button>

Но можно ли всё-таки оставить div и добавить ARIA-атрибуты? Нет, так делать не стоит. Во-первых, потому что первое правило ARIA гласит: не используйте атрибуты, если можно обойтись без них с помощью нативных html-тегов. Во-вторых, в таком случае вам придётся сделать такую разметку (так делать не нужно!) :

<div
  class="button"
  role="button"
  tabindex={0}
  onClick={doSomething}
  onKeyPress={doSomethingKeyPress}
>
  открыть фото котиков
</div>

Чтобы скринридер считывал кнопку, как кнопку, нужно добавить role="button". А чтобы пользователь клавиатуры смог сфокусироваться на кнопке с помощью таба, нужно добавить tabindex={0}. Кроме того, чтобы кнопку можно было нажать с помощью пробела или клавиши Enter, нужно добавить обработчик onKeyPress. По коду клавиши он определит, что именно нажал пользователь, и вызовет обработчик клика. Кажется, что это гораздо большая головная боль, чем сбросить background и border в css :)

Приведу ещё один пример, как делать не стоит:

<button
  class="button"
  role="button"
  tabindex={0}
  onClick={doSomething}
  onKeyPress={doSomethingKeyPress}
>
  открыть фото котиков
</button>

Если вы использовали тег button, то ни один из вышеперечисленных атрибутов вам не нужен. Тег по умолчанию воспринимается ассистивными технологиями как элемент с ролью button. При навигации табом он получит фокус, а при нажатии на пробел или Enter сработает обработчик onClick

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

А вот видео с неправильно размеченной кнопкой. Мы пытаемся сфокусироваться на стрелочке напротив имени, открывающей меню пользователя, но постоянно проскакиваем мимо неё.

Ссылки

Вторая по распространённости ошибка — ссылка без текста внутри. 

<a href={url} />

Если оставить такой кусочек кода, то скрин-ридер будет зачитывать адрес ссылки целиком. Вот три основных причины, почему появляется ссылка без подписи.

Ссылки-иконки

В иконках сами картинки создаются с помощью background...

<a href={url} />
a {
  background-image: url(...);
}

... или svg.

<a href={url}><svg>...</svg></a>

Тут нужно вставить текст в ссылку так, чтобы визуально ничего не изменилось, но при этом текст зачитался пользователям скрин-ридера. Для этого у нас есть компонент A11yHidden, с помощью которого мы вставляем скрытый текст. Сейчас объясню, как работает компонент. Например, такой jsx…

<A11yHidden hiddenText="фото котиков" />

…выводит на страницу следующий код:

<span class="A11yHidden">
  фото котиков
</span>
.A11yHidden {
  position: absolute;
  overflow: hidden;
  clip: rect(0,0,0,0);
  width: 1px;
  height: 1px;
  margin: -1px;
  white-space: nowrap;
}

Соответственно, чтобы исправить примеры выше, нужно добавить скрытый текст в ссылку с помощью этого компонента.

<a href={url}>
  <A11yHidden hiddenText="фото котиков" />
</a>

Или так:

<a href={url}>
  <A11yHidden hiddenText="фото котиков" />
  <svg>...</svg>
</a>

Ссылки-картинки

Вторая причина, по которой ссылка может остаться без подписи, — она оборачивает картинку без alt.

<a href={url} />
  <Image url={imageSrc} />
</a>

Решить проблему легко — просто добавьте alt :)

<a href={url} />
  <Image url={imageSrc} alt="Котик" />
</a>

Ссылки-оверлеи

Иногда я встречала пустые ссылки, которые абсолютом накладываются поверх других блоков, например:

Если ссылке задать display: block, то в неё можно вставлять блочные элементы. То есть решение этой проблемы — вставить кликабельные блоки внутрь ссылки.

<a href={url} className="ProductCard">
  <div className="ProductCard-ImageWrapper">
  <div className="ProductCard-ContentWrapper">
</a>

Если всё-таки нужно добавить ссылку-оверлей, но при этом ссылку нельзя вкладывать внутрь другой, то можно добавить скрытый текст. 

Как ещё не надо делать

Мне часто встречались ссылки с атрибутом tabindex="0". Этот атрибут нужен, чтобы пользователь клавиатуры смог сфокусироваться на кнопке с помощью таба, однако он не нужен ссылкам, потому что они фокусабельные по умолчанию.

<a tabIndex={0}>Фото котиков</a>

Вот ещё несколько примеров. В этом видео показано, как озвучиваются правильно размеченные ссылки (сайтлинки) и неправильные (промо в конце сниппета).

А в этом видео показано, как озвучиваются ссылки, внутри которых нет текстового описания или картинки. Картинка товара сделана через background, а следующая ссылка (информация под картинкой) — ссылка-оверлей.

Заголовки

Представьте, что у вас в руках огромная книга и вы хотите найти в ней определённую главу. Вы, конечно, откроете оглавление. Тот же принцип работает с веб-страницами: пользователи могут легко пробежать глазами и найти то, что им нужно с помощью заголовков. По тому же принципу работают и скринридеры.

Структура заголовков в текстовом документе всегда одинаковая. Сначала идёт главный заголовок первого уровня — h1. Чтобы сделать подразделы, нужно вставить заголовки поменьше — h2 или h3.

Незрячие пользователи перемещаются по странице с помощью заголовков, чтобы найти нужный им раздел, а понятная структура создаёт простое и понятное содержание страницы. 

А теперь перейдём к плохим примерам. Допустим, на странице есть главный заголовок, который соответствует уровню h3. Все остальные заголовки вам приходится размечать h4, h5 и меньше.

В итоге страница остаётся без заголовка первого уровня. И это неправильно. Тег заголовка должен соответствовать его значению, а не оформлению. В этом случае заголовки нужно начинать размечать с h1, но дать ему класс .h3, — он заставит его выглядеть так, как нужно. Главное, чтобы ваша дизайн-система позволяла это сделать.

Посмотрим на ещё один плохой пример. Допустим, у разделов страницы есть крупные заголовки, но сам заголовок страницы небольшой. Ошибкой в этом случае будет решение размечать заголовки разделов h1, а заголовок страницы оставить h3. Здесь решением снова будут стили. 

Картинки

Перед тем как перейти к разбору ошибок с картинками, хотелось бы в целом описать, как правильно их размечать. Начнём с того, что картинки бывают контентными и декоративными.

Контентные картинки несут в себе смысловую нагрузку: фотографии товаров, людей и прочего. Такие картинки должны быть размечены тегом с атрибутом alt, в котором должно быть текстовое описание того, что изображено на картинке.

Простая проверка. Представьте, что css отвалился и загрузились только картинки, указанные с помощью тега img. Утеряна ли в этом случае какая-то важная информация? Если да, то это контентная картинка.

Атрибут alt контентных картинок важен для пользователей скрин-ридера: им тоже очень интересно, что изображено на картинке. Например, в Твиттере у пользователей есть возможность задавать альтернативный текст к своим картинкам. К данной картинке сделана такая подпись: «Мем с презентацией Лизы Симпсон. Она стоит на сцене с решительным выражением лица. В презентации говориться, что доступность — это тоже работа дизайнера».

Декоративные изображения — это все картинки, которые улучшают внешний вид. Если они не загрузятся, пользователь ничего не потеряет. Например, это иконки в меню, которые дублируют названия элементов. Пользователям скрин-ридера о них знать необязательно, поэтому они не должны зачитываться. 

Декоративные картинки чаще всего делают тремя способами:

  1. css background;

  2. <img src=".." alt="">;

  3. svg.

Однако есть случаи, когда иконки приобретают значимость. Тогда нужно обозначить их существование для скринридера. Приведу два примера.

Изменение котировок. Обычно пользователь узнает, что цена акции выросла, по зелёному цвету и стрелке вверх. 

Незрячий пользователь видит только цену, и на сколько процентов она изменилась, но в какую сторону — непонятно. Вот как можно исправить ситуацию с помощью скрытого текста:

<p>
  <A11yHidden hiddenText="Цена акций выросла на" />
  10,00 ₽
</p>

Рейтинг. Иконка звёздочки говорит пользователю о том, что 4,8 — это рейтинг сайта. Это же работает с остальными сущностями — кафе, магазинами, товарами, отелями. 

Незрячий пользователь не понимает, что значит это число — оно зачитывается в отрыве от контекста. Чиним с помощью A11yHidden:

<p>
 <A11yHidden hiddenText="Рейтинг:" />
 4,8
</p>

Теги

Снова вспоминаем первое правило ARIA: не используйте ARIA-аттрибуты, если можно обойтись нативными html-тегами.

Иногда может встретиться довольно странный код маркированного списка:

<div role="heading" aria-level="2">Заголовок</div>
<div role="list">
  <div role="listitem">1</div>
  <div role="listitem">2</div>
  <div role="listitem">3</div>
</div>

Тут подошли бы старые добрые h2 и ul-li. Кроме того, с реализацией на ARIA гораздо легче допустить ошибку. Например, добавить role="list" для обёртки списка и забыть про role="listitem" — разметка будет сломана.

Сюда же можно отнести разметку таблиц с помощью div. Не стоит думать, раз табличная вёрстка уже никому не интересна, то не нужно использовать таблицы вовсе. Таблицы можно и нужно использовать при разметке табличных данных. Для незрячих пользователей это открывает возможность использовать кучу полезных шорткатов для навигации по таблице. По нажатию клавиши они смогут узнать, на какой строке находятся, как называется столбец и другие полезные сведения.

Заключение

В этой статье я рассмотрела базовые ошибки, которые можно пофиксить с помощью базовой разметки html. Если вам надо разработать какой-либо интерактивный элемент, лучше всего переиспользовать его из какой-нибудь известной дизайн-системы — компоненты из библиотек чаще всего сделаны с учётом доступности. Например, в react-bootstrap компоненты доступны — главное их не поломать. 

Если нужного компонента нет или вы наткнулись на более сложный случай, то нужно читать паттерн и разрабатывать решение в соответствии с ним. Вот несколько ссылок, которые могут помочь: