构建侧边导航组件

关于如何构建响应式滑动式侧边导航栏的基础概述

在这篇文章中,我想与您分享我是如何为 Web 原型设计一个侧边导航组件的,该组件具有响应性、状态性、支持键盘导航、在有和没有 JavaScript 的情况下都能工作,并且可以在各种浏览器中工作。试试演示

如果您更喜欢视频,这里是这篇文章的 YouTube 版本

概述

构建响应式导航系统非常困难。有些用户使用键盘,有些用户拥有强大的台式机,还有些用户通过小型移动设备访问。每位访问者都应该能够打开和关闭菜单。

桌面到移动响应式布局演示
iOS 和 Android 上的浅色和深色主题

Web 策略

在这个组件探索中,我很高兴地结合了一些关键的 Web 平台功能

  1. CSS :target
  2. CSS grid
  3. CSS transforms
  4. CSS 媒体查询,用于视口和用户偏好
  5. JS 用于 focus UX 增强功能

我的解决方案只有一个侧边栏,并且仅在“移动”视口为 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 及其 grid-area 语法,使我们能够将多个元素分配到同一行或同一列。

堆叠

主要布局元素 #sidenav-container 是一个网格,它创建 1 行和 2 列,其中 1 行和 1 列被命名为 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;
}

调整 2fr1fr 以找到您喜欢的菜单叠加层及其负空间关闭按钮的比率。

更改比率时会发生什么情况的演示。

CSS 3D 变换和过渡

我们的布局现在以移动视口大小堆叠。在我添加一些新样式之前,它默认情况下会覆盖我们的文章。以下是我在下一节中追求的一些 UX

  • 动画打开和关闭
  • 仅当用户同意时才使用动画
  • 动画 visibility,以便键盘焦点不会进入屏幕外元素

当我开始实现运动动画时,我想从最优先考虑无障碍性开始。

无障碍运动

并非每个人都想要滑动式运动体验。在我们的解决方案中,此偏好是通过在媒体查询中调整 --duration CSS 变量来应用的。此媒体查询值表示用户的操作系统对运动的偏好(如果可用)。

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

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}
应用和不应用持续时间的交互演示。

现在,当我们的侧边导航栏滑动打开和关闭时,如果用户偏好减少运动,我会立即将元素移动到视图中,在不运动的情况下保持状态。

Transition、transform、translate

侧边导航栏移出(默认)

要将移动设备上侧边导航栏的默认状态设置为屏幕外状态,我使用 transform: translateX(-110vw) 定位元素。

请注意,我在典型的屏幕外代码 -100vw 中添加了另一个 10vw,以确保侧边导航栏的 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 哈希更改时,在 var(--duration) 时间内将元素从其 -110vw 的“移出”位置滑动到其 0 的“移入”位置。

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

过渡 visibility

现在的目标是在菜单移出时对屏幕阅读器隐藏菜单,这样系统就不会将焦点放在屏幕外菜单中。我通过在 :target 更改时设置 visibility 过渡来实现此目的。

  • 移入时,不要过渡 visibility;立即变为可见,以便我可以查看元素滑动进来并接受焦点。
  • 移出时,过渡 visibility 但要延迟它,以便在过渡结束时将其翻转为 hidden

无障碍性 UX 增强功能

此解决方案依赖于更改 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();
})

当侧边导航栏打开时,将焦点放在关闭按钮上。当侧边导航栏关闭时,将焦点放在打开按钮上。我通过在 JavaScript 中调用元素的 focus() 来实现这一点。

结论

现在您知道我是如何做到的了,您会怎么做呢?!这构成了一些有趣的组件架构!谁将制作第一个带有插槽的版本?🙂

让我们使我们的方法多样化,并学习在 Web 上构建的所有方法。创建一个 Glitch在 Twitter 上给我发推文您的版本,我将其添加到下面的社区混音部分。

社区混音