Создание компонента боковой навигации

Базовый обзор того, как создать адаптивную выдвижную боковую навигацию

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

Если вы предпочитаете видео, вот версия этого поста на YouTube:

Обзор

Создать адаптивную систему навигации непросто. Некоторые пользователи будут использовать клавиатуру, у других — мощные компьютеры, а некоторые будут заходить на сайт с небольшого мобильного устройства. Каждый посетитель должен иметь возможность открывать и закрывать меню.

Демонстрация адаптивного макета для мобильных устройств и настольных компьютеров
Светлая и темная темы отключены на iOS и Android

Веб-тактики

В этом исследовании компонентов мне посчастливилось объединить несколько важнейших функций веб-платформы:

  1. CSS :target
  2. CSS- сетка
  3. CSS -преобразования
  4. CSS Media Requests для области просмотра и пользовательских предпочтений
  5. JS для улучшения UX- 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, благодаря синтаксису grid-area , позволяет назначать несколько элементов одной строке или столбцу.

Стеки

Основной элемент макета #sidenav-container — это сетка, которая создаёт одну строку и два столбца, каждый из которых называется stack . При ограниченном пространстве CSS присваивает всем дочерним элементам <main> одно и то же имя сетки, размещая все элементы в одном пространстве, создавая стек.

#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-преобразования и переходы

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

  • Анимация открытия и закрытия
  • Используйте анимацию с движением только в том случае, если пользователь с этим согласен.
  • Анимировать visibility , чтобы фокус клавиатуры не попадал на закадровый элемент

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

Доступное движение

Не всем нужен эффект выдвижения. В нашем решении эта настройка применяется путём настройки CSS-переменной --duration в медиазапросе. Это значение медиазапроса отражает настройки операционной системы пользователя для движения (если они доступны).

#sidenav-open {
  --duration: .6s;
}

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}
Демонстрация взаимодействия с применением длительности и без нее.

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

Переход, преобразование, перевод

Sidenav out (по умолчанию)

Чтобы установить состояние по умолчанию для нашей боковой навигации на мобильном устройстве на внеэкранное состояние, я размещаю элемент с помощью 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 в

Если элемент #sidenav совпадает с :target , установите позицию translateX() на homebase 0 и наблюдайте, как CSS перемещает элемент из его внешней позиции -110vw в его внутреннюю позицию 0 с помощью var(--duration) при изменении хеша URL.

@media (max-width: 540px) {
  #sidenav-open:target {
    visibility: visible;
    transform: translateX(0);
    transition:
      transform var(--duration) var(--easeOutExpo);
  }
}

Видимость перехода

Теперь моя цель — скрыть меню от скринридеров, когда оно открыто, чтобы системы не переводили фокус на закадровое меню. Я добился этого, установив переход видимости при изменении :target .

  • При входе не меняйте видимость; будьте видимы сразу, чтобы я мог видеть, как элемент появляется и принимает фокус.
  • При выходе из объекта сделайте его видимым, но задержите его, чтобы в конце выхода он стал 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>
Демонстрация UX-интерфейса с закадровым голосом и взаимодействием с клавиатурой.

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

:is(:hover, :focus)

Этот удобный функциональный псевдоселектор CSS позволяет нам быстро адаптировать стили наведения курсора, также разделяя их с фокусом.

.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-адресов при закрытии, и все будет выглядеть так, как будто меню никогда не открывалось.

Фокус UX

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

sidenav.addEventListener('transitionend', e => {
  const isOpen = document.location.hash === '#sidenav-open';

  isOpen
      ? document.querySelector('#sidenav-close').focus()
      : document.querySelector('#sidenav-button').focus();
})

Когда боковая навигация открывается, фокусируем кнопку закрытия. Когда боковая навигация закрывается, фокусируем кнопку открытия. Я делаю это, вызывая focus() для элемента в JavaScript.

Заключение

Теперь, когда вы знаете, как я это сделал, как бы поступили вы?! Получается интересная архитектура компонентов! Кто сделает первую версию со слотами? 🙂

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

Ремиксы сообщества