Хабр, я уже третий месяц пишу про доступность вместе с Ильёй. Мы показываем, как 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 не захламляется.


<form class="form">
  <!-- предыдущие элементы формы -->
  <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>
  <!-- последующие элементы формы -->
</form>

.field_required .field__label::after {
    content: "*";
    color: #e31420;
}

К сожалению, мне пришлось отказаться от него. Когда я протестировал мою форму скринридером JAWS, то он для каждого поля добавлял «звёздочка». Представляю удивление незрячего. Вот, что думает Илья о ней:


«Я знаю, что такое звёздочка, только по зрячему опыту. Есть множество людей, которые не понимают её значение. Единственным исключением является случай, когда над полем указывают пояснение, что звёздочка обозначает поля, обязательные для заполнения. Так что это в любом случае не универсальное решение, рассчитанное исключительно на визуальное восприятие».

Поэтому лучше её скрыть. Для этого нужно сначала добавить символ в HTML и скрыть с помощью атрибута aria-hidden.


<form class="form">
  <!-- предыдущие элементы формы -->
  <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>
  <!-- последующие элементы формы -->
</form>

.field__required-symbol {
    color: #e31420;
}

А совсем хорошо будет помочь пользователю скринридера узнать, что поле ввода обязательно для заполнения. Тем более, это очень просто сделать. Просто нужно добавить атрибут aria-required со значением true.


<form class="form">
  <!-- предыдущие элементы формы -->
  <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" aria-required="true" id="request_anonymous_requester_email">
  </div>
  <!-- последующие элементы формы -->
</form>

Теперь скринридер JAWS говорит: «Ваш email. Редактор обязательный. Введите текст».


▍ Заключение


С помощью этой статьи мы с Ильёй хотели призвать вас:


  • добавлять атрибут aria-labelledby и aria-label для элементов <section> и <form>;
  • не трогать порядок перемещения фокуса;
  • не забывать скрывать элементы, которые визуально появляются после взаимодействия пользователя;
  • скрывать «Звёздочку» у полей для пользователя скринридера.

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


Узнавайте о новых акциях и промокодах первыми из нашего Telegram-канала ????

Комментарии (25)


  1. Tzimie
    07.11.2023 09:39

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


  1. ifap
    07.11.2023 09:39
    +6

    Про aria-required Вы написали полнейшую дичь. Написанное и пример верны для простого required, а aria-required используется для случаев особых извращений, когда вся нормальная семантика идет по женскому половому признаку и в качестве элемента формы используется какой-нибудь, например, div, т.е. элемент, не являющийся по смыслу элементом формы, но зачем-то приспособленный автором к этой несвойственной для него роли. Тогда будет div aria-required="true" но в случае формы здорового человека будет просто input required.


  1. vanxant
    07.11.2023 09:39

    Продолжу тему, поднятую комментарием выше.

    Бредятина про section и aria-label[led-by] это явный косяк конкретного ридера. По стандрату HTML section обязан иметь хотя бы 1 элемент <hn>, который и будет заголовком секции. aria-тэги нужны, как правильно заметили выше, только для ситуаций с кривой вёрсткой. Однако ж одни рукожопы, начиная с авторов бутстрапа, их бездумно лепят куда попало, а потом другие не осилившие спецификацию не могут обрабатывать html код без aria-костылей.


    1. ifap
      07.11.2023 09:39

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


      1. vanxant
        07.11.2023 09:39

        validator.w3.org


        1. ifap
          07.11.2023 09:39

          По ссылке расположен валидатор гипертекстовой разметки, а не текст стандарта.


          1. vanxant
            07.11.2023 09:39

            Разумеется. И если скормить ему вёрстку с <section> без какого-нибудь <hn>, он ругнётся и приведёт ссылку на текст стандарта. Try it yourself!


            1. ifap
              07.11.2023 09:39

              Ругань валидатора даже от самого W3C не является стандартом HTML, коих у нас на сегодня 2: собственно, стандарт (Recommendation) от W3C и нечто аморфное от WHATWG, называемое "живым стандартом". Ни в одном не сказано, что Hx обязан находится внутри section.


            1. mayorovp
              07.11.2023 09:39
              +1

              Он приводит ссылку не на текст стандарта, а на справку по тэгам заголовков.

              Что же до требования иметь заголовок - это требование не HTML, а WAI-ARIA:

              Authors MUST give each element with role region a brief label that describes the purpose of the content in the region. Authors SHOULD reference a visible label with aria-labelledby if a visible label is present. Authors SHOULD include the label inside of a heading whenever possible. The heading MAY be an instance of the standard host language heading element or an instance of an element with role heading.


              1. vanxant
                07.11.2023 09:39
                -1

                Ну вот видите, вы сделали работу, которую господин@ifap пытался пассивно-агрессивно повесить на меня. Смысл в том, что никакие aria-атрибуты в данном случае не нужны, в html уже всё есть.


                1. mayorovp
                  07.11.2023 09:39

                  Нужны. Читайте ещё раз:

                  Authors SHOULD reference a visible label with aria-labelledby if a visible label is present.

                  Ну и до кучи ещё можно заглянуть в алгоритм вычисления Accessible Name: вы не найдёте там никаких упоминаний тэгов h1-h6.


                  1. vanxant
                    07.11.2023 09:39
                    -1

                    Нет. Нужны это MUST. SHOULD это благие пожелания, которые можно послать лесом (и 99.99% верстальщиков шлют).

                    В конечном счёте мерилом качества вёрстки является валидатор w3c (инструмент точный), а не кто там как интерпретирует строки стандарта (где на два юриста три мнения).


              1. ifap
                07.11.2023 09:39
                -1

                Ну то что мой собеседник понятия не имеет, о чем говорит, я подозревал, а он лишний раз подтвердил, вскриком выше, но Вы-то где увидели в процитированном фрагменте про заголовки (h) и секции (section)?! Я уж не говорю о том, что написанное относится лишь к частному случаю role="region"


                1. mayorovp
                  07.11.2023 09:39

                  В процитированном фрагменте написано, что элемент с ролью "region" обязан (MUST) иметь label, которую лучше бы (SHOULD) указывать как часть heading, которая может быть (MAY) standard host language heading element (вот вам и упоминание h_-тэга).

                  Секции напрямую в этом фрагменте не упоминаются, однако по умолчанию они как раз имеют роль "region" (если не нарушается процитированное правило).


                  1. ifap
                    07.11.2023 09:39

                    Это в чистом виде натягивание совы на глобус. Следуя этой логике, H-тег обязан быть внутри любого тэга с ролью регион, а это не так, в обозначенных целях вполне годится и aria-label, когда использование H-тега не предполагается для собственно визуального обозначения заголовка.


                    1. mayorovp
                      07.11.2023 09:39

                      Не обязан, но желателен.


                      1. ifap
                        07.11.2023 09:39

                        Ну наконец-то мы договорились: Hx в section не обязателен, уффф...


                      1. mayorovp
                        07.11.2023 09:39

                        Осталось понять с чем же вы спорили...


  1. tranklogic
    07.11.2023 09:39
    +1

    Без заголовков section можно подписать так же как form и прочие элементы, поддерживающие aria-label:

    <section aria-label="Контакты">
    Почта: <a href="mailto:mail@example.ru" >mail@example.ru</a>
    </section>
    

    NVDA читает: "Контакты Область"

    А с заголовком можно вообще не подписывать.
    С точки зрения навигации по странице, если есть заголовок, то не подписанная section - не проблема.
    Когда я перемещаюсь между заголовками с помощью клавиши "h" и нахожу "Контакты", то нет никакой разницы, как это семантически оформлено.

    С другой стороны, когда я ищу ориентиры с помощью клавиши "d", не подписанная section в ориентиры не попадает (в отличие от nav, main или footer)


    1. ifap
      07.11.2023 09:39

      Для контактов правильнее использовать специальный тэг address.


      1. tranklogic
        07.11.2023 09:39

        Уговорили )

        <section aria-label="Контакты">
          <address>
          Почта: <a href="mailto:mail@example.ru" >mail@example.ru</a>
          </address>
        </section>
        


        1. ifap
          07.11.2023 09:39

          Плохо уговорил: section тут лишнее, т.к. address с точки зрения семантики и есть section aria-label="контакты"


          1. tranklogic
            07.11.2023 09:39
            +1

            Речь шла о том, как скринридер озвучит именно section.
            address не является областью, у него другая роль.
            Если интересно, то

            <address aria-label="Контакты">
            Приёмная: <a href="tel:+222333222333" >+22 (233) 322-23-33</a>
              </address>
            

            звучит так: "Контакты Группа ..."
            В список семантических ориентиров address не попадает и по клавише "d" не ищется


            1. ifap
              07.11.2023 09:39

              Эвона как... тадыой, масло - масляное, контакты - контактные :(


              1. tranklogic
                07.11.2023 09:39

                Угу, контактные... ????