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

Базовый обзор того, как создать адаптивную слайд-сайд-панель.

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

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

Обзор

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

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

Веб-тактика

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

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

Стеки

Основной элемент макета #sidenav-container — это сетка, которая создает 1 строку и 2 столбца, по одному из которых называются 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> — это анимационный элемент, содержащий боковую навигацию. У него есть 2 дочерних элемента: навигационный контейнер <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 по умолчанию на мобильном устройстве в состояние «за кадром», я позиционирую элемент с помощью 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() значение 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>
Демонстрация пользовательского интерфейса озвучки и взаимодействия с клавиатурой.

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

: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.

Заключение

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

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

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