Ищем ба­ланс меж­ду на­тив­ным и ка­стом­ным се­лек­том

Перевод «Striking a Balance Between Native and Custom Select Elements»

Перевод Михаил Данюшин

Редактура Никита Дубко Вадим Макеев

Есть план! Мы сделаем стилизованный селект. Стилизуем не просто снаружи, но и внутри. Полный контроль над стилизацией. Вдобавок к этому мы сделаем его доступным. Мы не будем пытаться повторить за браузером всё, что он делает по умолчанию при отрисовке нативного <select>. Мы буквально будем использовать нативный <select>, как только используется любая вспомогательная технология. Но когда будет использоваться мышь, мы отрисуем стилизованную версию и заставим ее функционировать как <select>.

Вот что я понимаю под «гибридным» селектом: это одновременно и нативный <select>, и его стилизованная альтернатива.

Сравнение кастомного и нативного селектов: слева кастомный в стиле сайта, справа нативный в стиле ОС.
Кастомный селект часто используется вместо нативного ради эстетики и последовательности дизайна.

Селект, выпадающий список, навигация, меню… название имеет значение Скопировать ссылку

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

Перед тем, как мы двинемся дальше, позвольте мне внести ясность касательно использования термина «выпадающий список». Вот как я его понимаю:

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

Множество интерфейсов могут выглядеть похоже на выпадающий список. Но просто назвать элемент «выпадающим списком» — всё равно что использовать слово «рыба» для описания животного. Какое семейство рыб? Рыба-клоун не то же самое, что и акула. То же касается и выпадающих списков.

Кадр из мультфильма «Спасти Немо»: огромная зубастая акула за спиной двух крошечных испуганных рыбок.
Отличаем рыбу-клоуна от акулы.

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

  • Меню: список команд или действий, которые пользователь может исполнить на странице.
  • Навигация: список ссылок, используемых для перемещения по сайту.
  • Селект: контрол формы <select>, показывающий пользователю список опций, которые он может в ней выбрать.

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

Схема с примерами выпадающих списков: 1. Селект, 2. Меню, 3. Меню или селект, 4. Открывающаяся навигация, 5. Что-то другое.
Мир выпадающих списков: пять сценариев их использования в вебе. Более подробное описание — в таблице ниже.
Ожидаемое поведение Тип списка
1 Ожидается, что выбранный вариант отправится внутри формы на сервер, например, возраст. Селект
2 Выпадающему списку не нужен выбранный вариант, например, список действий: копировать, вставить и вырезать. Меню
3 Выбранный вариант влияет на контент, например, сортировка списка. Меню или селект, подробности чуть позже.
4 Выпадающий список содержит ссылки на другие страницы, например, большая навигация со ссылками на разделы сайта. Открывающаяся навигация
5 Содержимое выпадающего меню — не список, например, выбор даты. Что-то другое, что не следует называть выпадающим списком

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

Вот тип выпадающего списка, который определенно можно назвать меню. Его использование является горячей темой при обсуждении доступности. Я не буду много говорить об этом здесь, но позвольте мне просто подчеркнуть, что тег <menu> устарел и не рекомендуется к использованию. Вот подробное руководство по инклюзивным меню и меню-кнопкам (в переводе на «Веб-стандартах», прим. редактора), включая объяснение почему ARIA-роль menu не следует использовать для навигации по сайту.

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

Уфф… получилось много. Давайте забудем обо всём этом беспорядке с выпадающими списками и сосредоточимся исключительно на элементе <select>.

Давайте поговорим про <select> Скопировать ссылку

Стилизация элементов формы — увлекательное путешествие. Согласно MDN, есть хорошие, плохие и злые. К хорошим относится тег <form>, который попросту является блочным элементом. К плохим — чекбоксы, стилизация которых возможна, но громоздка. <select> определенно из области злых.

Про это написано огромное количество статей и даже в 2020 всё еще трудно создать кастомный селект и некоторые пользователи всё ещё предпочитают простые и нативные селекты.

Для разработчиков <select> — самый разочаровывающий элемент форм, главным образом из-за отстутствия поддержки стилизации. Борьба в UX за это настолько велика, что мы ищем альтернативы. Что ж, я думаю, что первое правило <select> такое же, как с ARIA: избегайте его использования, если можете.

Я могла бы закончить статью прямо сейчас словами «Не используйте <select>, точка». Но давайте посмотрим правде в глаза: селект для нас всё ещё лучшее решение в ряде случаев. Сюда можно отнести сценарии, когда мы работаем со списком, содержащим множество опций, раскладкой, ограниченной в пространстве, или же просто при нехватки времени или бюджета для разработки и реализации пользовательского интерактивного компонента с нуля.

Требования к кастомному <select> Скопировать ссылку

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

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

Что-то вроде такого:

Кто-то из вас подумает: «Работает и хорошо». Но постойте… Разве это работает для всех? Не все используют мышку (или тачскрин). К тому же нативный <select> обладает более широким списком возможностей, которые достаются нам бесплатно и не входят в этот список требований:

  • Выбранный вариант доступен для восприятия всеми пользователями, вне зависимости от их возможностей зрения.
  • С компонентом можно предсказуемо взаимодействовать с помощью клавиатуры во всех браузерах — например, используя клавиши стрелок для навигации, Enter для выбора, Esc для отмены и так далее.
  • Вспомогательные технологии (например, скринридеры) чётко объявляют пользователям элемент, называя его роль, имя и состояние.
  • Положение списка регулируется, то есть он не обрезается за краями экрана.
  • Элемент следует настройкам операционной системы пользователя — например, высокую контрастность, цветовую схему, ограничение движений и другие.

Именно на этом этапе большинство кастомных селектов так или иначе терпят крах. Взгляните на некоторые крупные UI-библиотеки. Я не буду упоминать конкретные, потому что веб достаточно недолговечный, но сходите попробуйте. Вероятно, вы заметите разное поведение селекта в разных фреймворках.

Вот дополнительные характеристики, за которыми нужно следить:

  • Выбирается ли опция списка сразу же при получения фокуса с клавиатуры?
  • Можно ли использовать Enter и Space для выбора варианта?
  • Нажатие на Tab переносит нас к следующему варианут списка или же к следующему элементу формы?
  • Что будет, когда вы достигнете последнего варианта в списке с помощью стрелок? Фокус замрет на последнем варианте, вернется к первому или же, что хуже всего, перейдет к следующему элементу формы?
  • Возможно ли перейти к последней опции списка с помощью клавиши Page Down?
  • Можно ли прокручивать элементы списка, если их больше, чем в поле видимости в данный момент?

Это был небольшой пример функций нативного селекта.

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

Но всё ещё хуже. Даже нативный <select> ведет себя по-разному в разных браузерах и скринридерах.

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

Создаём гибридный селект Скопировать ссылку

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

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

  • Нативный селект, видимый и доступный по умолчанию.
  • Кастомный селект, скрытый до тех пор, пока не произойдёт взаимодействие посредством мыши.

Начнём с разметки. Вначале, добавим нативный <select> с несколькими <option> до кастомного. Чуть позже я объясню почему.

Любой контрол формы должен содержать лейбл. Мы можем прибегнуть к <label>, но фокус будет попадать на нативный селект, когда мы будем кликать на подпись. В целях предотвращения такого поведения используем <span> и свяжем его с селектом с помощью aria-labelledby.

Наконец, с помощью aria-hidden="true" нужно сообщить вспомогательным технологиям, чтобы те игнорировали кастомный селект. Таким образом, они видят только нативный селект, несмотря ни на что.

<span class="selectLabel" id="jobLabel">
    Основная рабочая роль
</span>
<div class="selectWrapper">
    <select class="selectNative js-selectNative" aria-labelledby="jobLabel">
        <!-- Варианты -->
        <option></option>
    </select>
    <div class="selectCustom js-selectCustom" aria-hidden="true">
        <!-- Прекрасный кастомный селект -->
    </div>
</div>

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

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

.selectNative,
.selectCustom {
    position: relative;
    width: 22rem;
    height: 4rem;
}

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

.selectCustom {
    position: absolute;
    top: 0;
    left: 0;
    display: none;
}

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

@media (hover: hover) {
    .selectCustom {
        display: block;
    }
}

Отлично. Но что насчёт людей, которые используют клавиатуру для навигации даже на устройствах, поддерживающих ховер? Что делать? Мы будем прятать кастомный селект, когда нативный находится в состоянии фокуса. Мы можем поймать соседний элемент с помощью комбинирующего селектора +. Как только нативный селект в фокусе, прячем кастомный, который следует сразу за ним в DOM. Вот почему кастомный селект должен следовать за нативным.

@media (hover: hover) {
    .selectNative:focus + .selectCustom {
        display: none;
    }
}

Вот и всё! Трюк переключения между двумя селектами готов. Есть другие способы сделать это через CSS, но и этот прекрасно работает.

Наконец, нам нужно немного JavaScript. Добавим несколько обработчиков событий:

  • Один для события клика, по которому в игру вступает кастомный селект, раскрываясь и показывая варианты выбора.
  • Один, чтобы синхронизировать выбранные варианты. При изменении одного варианта выбора, меняется и второй.
  • И ещё один для установки навигации через клавиатуру с помощью клавиш Up и Down, выбора варианта с помощью клавиш Enter или Space, и закрытия списка через Esc.

Юзабилити-тест Скопировать ссылку

Я провела небольшое юзабилити-тестирование, в котором я попросила нескольких людей с ограниченными возможностями воспользоваться гибридным селектом. Были протестированы следующие устройства и инструменты с использованием последних версий Chrome 81, Firefox 76, Safari 13:

  • Компьютер только с мышью.
  • Компьютер только с клавиатурой.
  • VoiceOver на macOS с помощью клавиатуры.
  • NVDA в Windows с помощью клавиатуры.
  • VoiceOver на iPhone и iPad в Safari

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

Во время теста была обнаружена проблема. В частности, проблема связана с настройкой VoiceOver «Использовать виртуальный курсор VoiceOver». Если пользователь откроет селект с помощью этого курсора, вместо нативного покажется кастомный селект.

Больше всего мне нравится в этом подходе то, как он совмещает всё самое лучшее из обоих миров без нанесения ущерба функциональности.

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

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

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

Примечание касательно селекта-меню Скопировать ссылку

Давайте вернёмся к третьему сценарию нашего списка селектов. Если вы помните, это выпадающий список, который всегда имеет отмеченный вариант (например, сортировка). Я отнесла его к серой области как и меню или селект.

Идея такая: много лет назад этот тип выпадающего списка реализовывался в основном с помощью нативного <select>. В настоящее время часто можно увидеть что он реализован с нуля с помощью кастомных стилей (доступных или нет). И мы получаем селект, стилизованный под меню.

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

<select> — это вид меню. Оба имеют схожую семантику и поведение, особенно в случае, когда один вариант всегда выбран. Теперь позвольте мне упомянуть критерий из WCAG 3.2.2 о полях (уровень A):

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

Давайте применим это на практике. Представьте себе сортируемый список студентов. Может быть визуально очевидно, что сортировка происходит незамедлительно, но это не обязательно так для всех людей. Таким образом, при использовании <select>, мы рискуем нарушить правила WCAG, поскольку контент страницы изменился, а это попадает под понятие «изменение контекста».

Чтобы соблюсти критерий, мы должны уведомить пользователя о действии до того, как он начнёт взаимодействовать с элементом или же поставить <button> сразу после списка, чтобы подтвердить изменения.

<label for="sortStudents">
    Сортировка студентов
    <!--
        Предупреждение для пользователя,
        если нет кнопки подтверждения.
    -->
    <span class="visually-hidden">
        (Немедленный эффект после выбора)
    </span>
</label>
<select id="sortStudents">
    <!-- Опции сортировки -->
</select>

Тем не менее, использование <select> наряду с созданием пользовательского меню является хорошим подходом, когда речь заходит о несложных меню, требующих изменение содержимого страницы. Просто помните, что от вашего решения зависит объём работ, необходимых для создания полностью доступного компонента. Это как раз тот случай, когда гибридный селект может выручить.

Заключение Скопировать ссылку

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

Создание действительно доступных селектов (или же любого вида выпадающего списка) сложнее, чем может казаться. Руководство WCAG даёт прекрасные инструкции наряду с лучшими практиками, но без конкретных примеров использования эти инструкции носят рекомендательный характер. Не говоря уже о том, что поддержка ARIA слабая, а внешний вид и поведение браузерного <select> отличаются в разных браузерах.

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

P.S. Не забудьте выбрать правильное название при создании выпадающего списка 😉