Выезжающая боковая панель (sidebar)

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

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

Веб-тактика

В компоненте выезжающей боковой панели (sidebar) предстоит объединить несколько важных функций веб-платформы:

  1. CSS :target
  2. CSS сетка
  3. CSS преобразования
  4. CSS Media Queries для области просмотра и пользовательских предпочтений
  5. JS для focus (UX-улучшение)

В рассматриваемом решении выезжающая боковая панель с переключателем будет только для «мобильного» окна шириной до 540 пикселей. 540 пикселей будет точкой остановки для переключения между интерактивным (для мобильного) и статическим (для десктопа) макетами. В статическом макете sidebar присутствует в виде колонки с навигацией.

CSS-псевдокласс :target

Начнем с переключателя. Для него надо добавить две ссылки <a> с значениями атрибута href, соответственно: #sidenav-open и #. Для HTML-элемента боковой панели надо добавить атрибут id="sidenav-open":

<a href="#sidenav-open"
  id="sidenav-button"
  title="Открыть меню"
  aria-label="Открыть меню"></a>

<a href="#"
  id="sidenav-close"
  title="Закрыть меню"
  aria-label="Закрыть меню"></a>

<aside id="sidenav-open">
  …
</aside>

Клик по каждой из этих ссылок изменяет хэш для URL страницы, а затем с помощью CSS-псевдокласса показывается или скрывается боковая панель навигации:

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
  }

  #sidenav-open:target {
    visibility: visible;
  }
}
Изменнение хэша в URL страницы.

CSS grid

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

Сетка

Основной элемент макета #sidenav-container представляет собой сетку, которая создает 1 строку и 2 столбца с названием stack для первой.

#sidenav-container {
  display: grid;
  grid: [stack] 1fr / min-content [stack] 1fr;
  min-height: 100vh;
}

@media (max-width: 540px) {
  #sidenav-container > * {
    grid-area: stack;
  }
}

Подложка

Анимированный HTML=элемент <aside> — боковая панель навигации. У него есть 2 дочерних элемента: контейнер с навигацией <nav> с названием [nav] и подложка <a> с названием [escape], назначение которой — закрывать панель с меню.

#sidenav-open {
  display: grid;
  grid-template-columns: [nav] 2fr [escape] 1fr;
}
Демонстрация происходящего от смены соотношения.

CSS 3D-преобразования и переходы

Теперь панель навигации на мобильном экране накладывается поверх контента. Для sidebar надо добавить ещё немного CSS. Вот UX-цели, который разберём в следующем разделе:

  • Анимировать разворачивание/сворачивание
  • Анимировать с плавным движением, только если пользователь не возражает
  • Анимировать visibility, чтобы фокус клавиатуры не исчезал за пределы экрана

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

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

Кто-то не любит плавное движение при появлении боковой панели и предпочитает быстрое появление sidebar. Такое предпочтение можно учитывать с помощью настройки внутри медиа-запроса prefers-reduced-motion: reduce значения CSS-переменной --duration. Это учитывает предпочтения пользователя для движения (если доступно), настроенные в операционной системе.

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

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

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

Transition, transform, translate

Панель скрыта (по умолчанию)

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

Панель открыта

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

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

Изменение видимости

Следующая цель — реализовать невидимость меню, когда sidebar скрыт, от программ чтения с экрана. Это нужно, чтобы фокус не переводился на меню, скрытое за пределами экрана. Для этого можно изменять CSS-свойство visibility панели при изменениях :target:

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

Улучшение UX-доступности

Ссылки

Поскольку рассматриваемое решение полагается на изменение URL-адреса для управления состояниями, здесь следует использовать HTML-элемент <a>, у которого достаточно полезных для доступности функций. Можно немного украсить интерактивные элементы, чтобы намерения выражались яснее.

<a href="#" id="sidenav-close"
  title="Закрыть меню"
  aria-label="Закрыть меню"></a>

<a href="#sidenav-open" id="sidenav-button"
  class="hamburger"
  title="Открыть меню"
  aria-label="Открыть меню">
  <svg>...</svg>
</a>
Демонстрация озвучивания и UX-взаимодействия с клавиатурой.

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

:is(:hover, :focus)

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

.hamburger:is(:hover, :focus) svg > line {
  stroke: hsl(var(--brandHSL));
}

Немного Javascript

Кнопка Esc для сворачивания

Клавиша Escape на клавиатуре должна закрывать меню, например так:

const sidenav = document.querySelector('#sidenav-open');

sidenav.addEventListener('keyup', event => {
  if (event.code === 'Escape') document.location.hash = '';
});

UX-фокус

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

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

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

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

Пример выезжающей боковой панели:

See this code Building a sidenav component on x.xhtml.ru.