Фундаментальный обзор того, как создать адаптивный выдвигающийся компонент sidenav
В этой статье я хочу поделиться с вами своим способом создания адаптивной боковой панели навигации (sidenav), которая отслеживает состояние, поддерживает управление с клавиатуры, работает с JavaScript и без него и поддерживается разными браузерами. Попробуйте демонстрацию.
Если вы предпочитаете видео, можете посмотреть видеоверсию этой статьи на YouTube:
Обзор
Создать адаптивную систему навигации непросто. Некоторые пользователи могут работать с помощью клавиатуры, одни при входе на сайт будут использовать мощный компьютер, другие — маленькое мобильное устройство. Но каждый из посетителей должен иметь возможность открыть и закрыть меню.
Веб-подходы
При исследовании этого компонента я совместил несколько важных концепций веб-разработки:
- CSS-псевдокласс
:target
- CSS Grid
- CSS-трансформации
- CSS-медиазапросы для области просмотра и пользовательских предпочтений
- JS для улучшения удобства использования
focus
В моем решении присутствует одна боковая панель, которая выдвигается только в «мобильной» области просмотра шириной 540px
или меньше. Размер 540px
будет нашей контрольной точкой для переключения между интерактивным макетом для мобильных и статическим для десктопов.
CSS-псевдокласс :target
Одна ссылка <a>
устанавливает в URL-хеш значение #sidenav-open
, а вторая — пустое значение (''
). У самого элемента имеется id
, соответствующий этому хешу:
<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 столбца, 1 из которых получает имя 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 3D-преобразования и переходы
Теперь наш макет умещается и в размер мобильной области просмотра. Пока я не добавлю несколько новых стилей, боковая панель по умолчанию будет накладываться на нашу статью. Вот функционал, к которому я стремлюсь в следующем разделе:
- Анимированное открытие и закрытие
- Анимация только в том случае, если пользователь ее не отключает
- Анимирование
visibility
, чтобы фокус клавиатуры не выходил за пределы экрана
Поскольку я приступаю к реализации анимированного движения, в первую очередь давайте начнем с доступности.
Доступная анимация
Не всем захочется видеть анимацию выдвигающегося меню. В нашем решении предпочтение пользователя применяется путем настройки CSS-переменной --duration
внутри медиазапроса. Значение этого медиазапроса представляет предпочтения операционной системы пользователя в отношении анимации (если они доступны).
#sidenav-open {
--duration: .6s;
}
@media (prefers-reduced-motion: reduce) {
#sidenav-open {
--duration: 1ms;
}
}
Теперь, когда наша боковая навигация открывается и закрывается, если пользователь предпочитает ограничить анимацию, я мгновенно перемещаю элемент в область просмотра, поддерживая тем временем состояние без движения.
Переход, трансформация, трансляция
Боковая панель закрыта (по умолчанию)
Чтобы на мобильных устройствах наша панель боковой навигации по умолчанию находилась за пределами экрана, я позиционирую элемент с помощью transform: translateX(-110vw)
.
Обратите внимание, я добавил еще 10vw
к типичному закадровому коду -100vw
, чтобы гарантировать, что тень box-shadow
блока боковой навигации не видна в основной области просмотра, когда панель скрыта.
@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 при изменении URL-хеша сместит элемент с его исходной позиции -110vw
в позицию «открыто», равную 0
, в течение времени, установленного в переменной var(--duration)
.
@media (max-width: 540px) {
#sidenav-open:target {
visibility: visible;
transform: translateX(0);
transition:
transform var(--duration) var(--easeOutExpo);
}
}
Переход свойства visibility
Теперь, когда панель находится за пределами области просмотра, ее нужно скрыть от программ чтения с экрана, чтобы они не переводили фокус на элементы закадрового меню. Я реализовал это с помощью перехода свойства visibility при смене псевдокласса :target
.
- При открытии применять переход не нужно; сразу видимая панель должна выезжать из-за пределов экрана и получать фокус.
- При закрытии для свойства visibility нужно применить переход, но с задержкой, чтобы панель стала невидимой (
hidden
) в конце перехода.
Улучшения доступности
Ссылки
Это решение основывается на изменении 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)
Этот удобный функциональный псевдоселектор CSS позволяет нам задать стили одновременно для состояний 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 = '';
});
История браузера
Чтобы каждое открытие и закрытие панели не создавало отдельную запись в истории браузера, добавьте следующий встроенный JavaScript-код для кнопки закрытия:
<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>
Это приведет к удалению записи истории URL-адресов при закрытии панели, как если бы меню никогда не открывалось.
Фокус
Следующий фрагмент помогает нам поместить фокус на кнопки открытия и закрытия при соответствующем действии панели. Я хочу упростить переключение.
sidenav.addEventListener('transitionend', e => {
const isOpen = document.location.hash === '#sidenav-open';
isOpen
? document.querySelector('#sidenav-close').focus()
: document.querySelector('#sidenav-button').focus();
})
Когда боковая панель открывается, фокус попадает на кнопку закрытия. Когда же панель закрывается, фокус попадает на кнопку открытия. Я делаю это с помощью JavaScript, вызывая focus()
для элемента.
Заключение
Теперь вы знаете о моем подходе в реализации этого компонента. Как бы его реализовали вы? Тут есть пространство для творчества. Кто же сделает первую версию со слотами? 🙂
Давайте разнообразим наши подходы и найдем все способы создания компонентов. Создайте демонстрацию на Glitch, напишите мне о своей версии в Твиттере, и я добавлю ее в раздел ремиксов сообщества ниже.
Ремиксы сообщества
- @_developit с настраиваемыми элементами: демонстрация и код
- @mayeedwin1 с HTML/CSS/JS: демонстрация и код
- @a_nurella с ремиксом на Glitch: демонстрация и код
- @EvroMalarkey с HTML/CSS/JS: демонстрация и код