Приветствую. Представляю вашему вниманию перевод статьи «Building a sidenav component», опубликованной 21 января 2021 года автором Adam Argyle
В данной статье я хочу поделиться одним из способов создания адаптивной боковой панели навигации (sidenav), поддерживающей управление с клавиатуры, работающей как с JavaScript, так и без него, и поддерживаемой всеми браузерами. Посмотреть демонстрацию можно здесь
Если Вы предпочитаете видео, ниже представлена YouTube-ролик по данной статье:
Обзор
Создать адаптивную навигацию непросто. Некоторые пользователи могут работать с помощью клавиатуры, одни при входе на сайт будут использовать мощный компьютер, другие — маленькое мобильное устройство. Но каждый из посетителей должен иметь возможность открыть и закрыть меню.
Демонстрация адаптивности макета на десктопе и мобильных:
Светлая и тёмная тема на iOS и Android
Подходы
При исследовании данного компонента я совместил несколько концепций веб-разработки:
CSS-псевдокласс
:target
CSS Grid
CSS-трансформации
CSS-медиазапросы для области видимости и предпочтений пользователей
JS для увеличения удобства использования
В моём решении на больших экранах боковая панель статична, а "выезжающей" становится только когда ширина области видимости меньше 540px
. Данный размер будет контрольной точкой для переключения между интерактивной раскладкой для мобильных и статической для десктопов.
CSS-псевдокласс :target
Ссылка <a>
, открывающая панель, устанавливает в URL-хэш значение #sidenav-open
. У самого же элемента боковой панели имеется id
, который соответствует этому значению. Закрывающая ссылка устанавливает в URL-хэш пустое значение (''
):
<a href="#sidenav-open" id="sidenav-button" title="Open Menu" aria-label="Open Menu">
<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>
<aside id="sidenav-open">
…
</aside>
Нажатие на эти ссылки изменяет состояние (отображение или скрытие) боковой панели в зависимости от URL в адресной строке:
@media (max-width: 540px) {
#sidenav-open {
visibility: hidden;
}
#sidenav-open:target {
visibility: visible;
}
}
CSS Grid
Раньше для компонента боковой панели я использовал только абсолютное или фиксированное позиционирование. Технология CSS Grid с её синтаксисом grid-area
, открывает ещё один способ, позволяя нам на одну строку или колонку назначить несколько элементов.
Стопки
Основной элемент макета #sidenav-container
является grid-элементом, который создаёт 1 строку и 2 колонки, а первая ячейка получает имя stack
. Когда пространство ограничено, CSS назначает все элементы, дочерние по отношению к <main>
в одну и ту же grid-область, размещая все элементы в одну ячейку в виде стопки.
#sidenav-container {
display: grid;
grid: [stack] 1fr / min-content [stack] 1fr;
min-height: 100vh;
}
@media (max-width: 540px) {
#sidenav-container > * {
grid-area: stack;
}
}
Подложка
<aside>
— это анимированный элемент, который содержит боковую навигацию. У него есть два дочерних элемента: контейнер <nav>
, которому задано имя [nav]
и подложка <a>
с именем [escape]
, которая используется для закрытия меню.
#sidenav-open {
display: grid;
grid-template-columns: [nav] 2fr [escape] 1fr;
}
С помощью изменения значений 2fr
& 1fr
можно настроить соотношение между панелью и оставшимся пространством при открытом боковом меню.
Демонстрация результата изменения размера панели
CSS трансформации и переходы
Теперь наш макет умещается и в небольшую область видимости мобильного устройства. Пока что боковая панель по умолчанию накладывается на основное содержимое. Вот функционал, который я хочу дополнить в следующем разделе:
Анимированное открытие и закрытие
Анимация только в том случае, если пользователь не предпочитает её отключать
Анимирование
visibility
, чтобы фокус клавиатуры не выходил за пределы экрана
Так как дело дошло до реализации анимированного движения, прежде всего я бы хотел начать с доступности
Доступная анимация
Не все хотят видеть анимацию выезжающего меню. В нашем примере от предпочтения пользователя зависит значение CSS-переменной --duration
, определяющей длительность анимации. Данная переменная находится внутри медиазапроса, который учитывает настройки операционной системы пользователя.
#sidenav-open {
--duration: .6s;
}
@media (prefers-reduced-motion: reduce) {
#sidenav-open {
--duration: 1ms;
}
}
Демонстрация работы интерфейса с разными настройками анимирования
Теперь, если пользователь предпочитает уменьшенное количество анимации, панель будет появляться мгновенно.
Переход, трансформация, смещение
Скрытая панель (по умолчанию)
Чтобы на мобильных по умолчанию панель находилась за пределами экрана, я смещаю её с помощью transform: translateX(-110vw).
Обратите внимание, что в дополнение к обычной ширине области видимости -100vw
я дополнительно добавил 10vw
, чтобы быть уверенным, что в скрытом состоянии тень от боковой панели не будет видна на экране.
@media (max-width: 540px) {
#sidenav-open {
visibility: hidden;
transform: translateX(-110vw);
will-change: transform;
transition:
transform var(--duration) var(--easeOutExpo),
visibility 0s linear var(--duration);
}
}
Открытая панель
Когда элемент #sidenav
соответствует псевдоклассу :target
, установите позиционирование с помощью translateX()
на стандартное значение 0
и посмотрите, как CSS в течение времени, установленного в переменной var(--duration)
, сместит элемент с его исходной позиции "скрыто", равной -110vw
в позицию "открыто", равную 0
.
@media (max-width: 540px) {
#sidenav-open:target {
visibility: visible;
transform: translateX(0);
transition:
transform var(--duration) var(--easeOutExpo);
}
}
Переход для свойства visibility
Теперь, когда панель находится за пределами области видимости, её нужно скрыть и от скринридеров, чтобы они не переводили фокус на её элементы. Я реализовал это с помощью перехода (transition) для свойства visibility
, который выполняется при смене псевдокласса :target
.
При открытии переход применять не нужно, чтобы сразу видимая панель выезжала из-за пределов экрана
При закрытии панели для свойства
visibility
нужно применить переход, но с задержкой, чтобы она становилась невидимой только после завершения перехода
Улучшения доступности
Ссылки
Для управления состоянием панели, приведённое выше решение полагается на изменения URL-адреса. Естественно, здесь нужно использовать элемент <a>
, который имеет некоторые преимущества в плане доступности. Давайте дополним наши интерактивные элементы доступными подписями, отражающими их назначение.
<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>
<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">
<svg>...</svg>
</a>
Демонстрация взаимодействия с помощью клавиатуры и скринридера
Теперь наши основные кнопки взаимодействия содержат понятное обозначение для пользователей как мыши, так и клавиатуры.
:is(:hover, :focus)
Этот удобный псевдокласс позволяет нам задать стили одновременно для состояний hover
и focus
.hamburger:is(:hover, :focus) svg > line {
stroke: hsl(var(--brandHSL));
}
Добавление JavaScript
Escape для закрытия
Кнопка Escape
на клавиатуре должна закрывать меню, правильно? Давайте реализуем такую возможность
const sidenav = document.querySelector('#sidenav-open');
sidenav.addEventListener('keyup', event => {
if (event.code === 'Escape') document.location.hash = '';
});
История браузера
Чтобы каждое открытие и закрытие панели не создавало отдельную запись в истории браузера, для кнопки закрытия добавьте следующий код
<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>
При закрытии панели запись в истории будет удалена, как если бы панель никогда и не открывалась
Фокус
Следующий фрагмент кода помогает нам поместить фокус на кнопки открытия и закрытия при соответствующем действии панели. Я хочу сделать переключение простым
sidenav.addEventListener('transitionend', e => {
const isOpen = document.location.hash === '#sidenav-open';
isOpen
? document.querySelector('#sidenav-close').focus()
: document.querySelector('#sidenav-button').focus();
})
Когда боковая панель открывается, фокус попадает на кнопку закрытия. Когда же панель закрывается, фокус попадает на кнопку открытия. Я делаю это с помощью JavaScript, вызывая на элементе focus()
.
Заключение
Теперь вы знаете о моём подходе в реализации данного компонента. Как бы его реализовали Вы?
ssurrokk
если вы переводите "responsive" как "отзывчивый" вместо "адаптивный", то как тогда называть интерфейс который плохо реагирует на действия пользователя, не отзывается на его действия? Ну вы поняли)
hisbvdis Автор
Заменил на "адаптивный".
Спасибо