构建侧边导航栏组件

简要介绍如何构建响应式滑出侧边导航栏

在这篇博文中,我想与您分享我是如何为 Web 构建一款 Sidenav 组件的原型,该组件具有响应式、有状态、支持键盘导航、无论是否使用 JavaScript 以及跨浏览器都能正常运行。试用演示

如果你更喜欢视频,可以参考本博文的 YouTube 版本:

概览

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

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

网络策略

在此组件探索中,我很高兴地将一些关键的 Web 平台功能结合在一起:

  1. CSS :target
  2. CSS 网格
  3. CSS transforms
  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;
}

调整 2fr1fr,找到您想要的菜单叠加层及其负空间关闭按钮的比例。

演示更改比率后的情况。

CSS 3D 转换和转场

现在,我们的布局会以移动设备视口尺寸叠加显示。在我添加一些新样式之前 默认情况下,样式会叠加在我的文章上在下一部分,我将力求实现一些用户体验:

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

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

无障碍动作

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

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

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

现在,当侧边导航栏滑动打开和关闭时,如果用户喜欢减少动作,我可以立即将元素移至视图,保持无动作的状态。

转换、转换、翻译

Sidenav out(默认)

为了在移动设备上将侧边导航栏的默认状态设置为离屏状态,应使用 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 将元素从其 -110vw 的出站位置滑动到其 0 的“in”位置(在 var(--duration) 上)。

@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,将您的版本发推给我,然后我就会将其添加到下面的社区混剪部分。

社区混剪作品