构建侧边导航栏组件

关于如何构建自适应滑出式侧边导航栏的基础概览

在本文中,我想与您分享我是如何为 Web 设计 Sidenav 组件原型的,该组件具有自适应、有状态、支持键盘导航、无论是否使用 JavaScript 都能正常运行以及跨浏览器运行等特点。试用演示版。

如果您更喜欢视频,可以观看此博文的 YouTube 版本:

概览

构建自适应导航系统并非易事。有些用户会使用键盘,有些用户会使用功能强大的桌面设备,还有些用户会使用小型移动设备访问。所有访问者都应该能够打开和关闭菜单。

桌面设备到移动设备的自适应布局演示
iOS 和 Android 上的浅色和深色主题

网站策略

在这次组件探索中,我很高兴能将几个关键的 Web 平台功能结合在一起:

  1. CSS :target
  2. CSS 网格
  3. CSS 转换
  4. 用于视口和用户偏好的 CSS 媒体查询
  5. focus 用户体验增强功能的 JS

我的解决方案有一个边栏,并且仅在“移动设备”视口宽度不大于 540px 时切换。 540px 将作为在移动互动式布局和静态桌面布局之间切换的断点。

CSS :target 伪类

一个 <a> 链接将网址哈希设置为 #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>

点击这些链接会更改网页网址的哈希状态,然后通过伪类显示和隐藏侧边栏:

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

  #sidenav-open:target {
    visibility: visible;
  }
}

CSS 网格

过去,我只使用绝对或固定位置的侧边栏布局和组件。不过,网格通过其 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> 和名为 [escape] 的背景幕 <a>,用于关闭菜单。

#sidenav-open {
  display: grid;
  grid-template-columns: [nav] 2fr [escape] 1fr;
}

调整 2fr 和 1fr,找到适合菜单叠加层及其负空间关闭按钮的比例。

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

CSS 3D 转换和过渡

现在,我们的布局在移动设备视口大小下会堆叠。在我添加一些新样式之前,它会默认叠加在文章上。以下是我在下一部分中要实现的一些用户体验:

  • 打开和关闭动画
  • 仅在用户不介意的情况下使用动态效果
  • 为 visibility 添加动画效果,使键盘焦点不会进入屏幕外元素

在开始实现动作动画时,我希望首先考虑无障碍功能。

无障碍动效

并非所有人都希望获得滑出式动作体验。在我们的解决方案中,此偏好设置是通过在媒体查询中调整 --duration CSS 变量来应用的。此媒体查询值表示用户对动画效果的操作系统偏好设置(如果可用)。

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

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}
应用和未应用时长时的互动演示。

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

过渡、转换、平移

侧边栏滑出(默认)

为了将移动设备上的侧边导航栏的默认状态设置为屏幕外状态,我使用 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() 位置设置为 homebase 0,并观察 CSS 如何在网址哈希更改时,在 var(--duration) 内将元素从其 -110vw 的“out”位置滑动到其 0 的“in”位置。

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

过渡可见性

现在的目标是在菜单移出屏幕时对屏幕阅读器隐藏菜单,这样系统就不会将焦点放在屏幕外的菜单上。为此,我会在 :target 发生变化时设置可见性过渡。

  • 进入时,不要过渡可见性;立即显示,以便我可以看到元素滑入并接受焦点。
  • 在退出时,转换可见性,但延迟转换,以便在退出转换结束时切换到 hidden。

无障碍用户体验增强功能

此解决方案依赖于更改网址来管理状态。 自然,这里应该使用 <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>

这会在关闭时移除网址历史记录条目,就像从未打开过该菜单一样。

专注用户体验

下一个代码段可帮助我们在打开和关闭按钮打开或关闭后将焦点置于这些按钮上。我想让切换变得简单。

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 上向我发送您的版本,我会将其添加到下面的社区混剪部分。

社区混剪