关于如何构建自适应滑动侧边导航栏的基础概览
在这篇博文中,我想与大家分享我如何为 Web 设计了一个侧边栏组件原型,该组件具有响应性、有状态、支持键盘导航、可在有无 JavaScript 的情况下运行,并且可在各种浏览器中运行。试用演示版。
如果您更喜欢视频,可以观看此帖子的 YouTube 版本:
概览
构建自适应导航系统非常困难。有些用户会使用键盘,有些用户会使用功能强大的桌面设备,还有些用户会通过小型移动设备访问。 所有访问者都应能够打开和关闭菜单。
网络策略
在此组件探索中,我很高兴能将几个关键的 Web 平台功能结合起来:
我的解决方案有一个边栏,仅当视口宽度为 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 列,其中 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>
和名为 [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 in
当 #sidenav
元素匹配为 :target
时,将 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);
}
}
过渡效果的可见性
现在的目标是在菜单处于隐藏状态时,向屏幕阅读器隐藏该菜单,这样系统就不会将焦点放在屏幕外的菜单上。我通过在 :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 上私信我你的版本,我会将其添加到下方的社区混音部分。
社区混音作品
- @_developit 使用自定义元素:演示和代码
- @mayeedwin1 使用 HTML/CSS/JS:演示和代码
- @a_nurella 的 Glitch Remix:演示和代码
- @EvroMalarkey 使用 HTML/CSS/JS:演示和代码