在任何 Web 框架中构建美观且适合移动设备的导航栏
我最近一直在构建更多的静态网站,每个网站都需要同样的东西:
- 一个漂亮且响应迅速的导航栏,左侧有徽标,右侧有链接💪
- 对于移动屏幕,将右侧的链接折叠到带有下拉菜单的汉堡菜单中🍔
- 满足所有可访问性要求:语义 HTML、键盘导航等等♿️
- 添加一些精美的动画,营造时尚、现代的感觉✨
哦,对了,还要用团队用的任何框架来实现它。这听起来可能有点吓人……但在React、Svelte和原生 JS 之间反复尝试之后,我想我找到了一个无论你走到哪里都能用得上的可靠解决方案。
前进!
首先,最终目标是什么?
这是我最近的项目的屏幕截图:重新设计Hack4Impact非营利网站。
别管猫了。我们等内容的时候需要一些完美的占位符😼
它有一些奇特的花哨功能,例如背景模糊效果,但它涵盖了我们所追求的一般“公式”!
放置一些 HTML
让我们首先定义导航栏的总体结构。
<nav>
<a class="logo" href="/">
<img src="dope-logo.svg" alt="Our professional logo (ideally an svg!)" />
</a>
<button class="mobile-dropdown-toggle" aria-hidden="true">
<!-- Cool hamburger icon -->
</button>
<div class="dropdown-link-container">
<a href="/about">About Us</a>
<a href="/work">Our Work</a>
...
</div>
</nav>
这里有几点需要注意:
- 我们这里没有使用无序列表 (ul) 作为链接。你可能在网上看到过类似的建议,这当然是合理的!不过,Chris Coyier 这篇支持/反对意见的细致文章让我更加坚定了想法。简而言之:列表并非A11y 的必需品(这个问题最多也只是个小问题),所以如果有合理的理由,我们可以放弃它们。在我们的例子中,我们实际上需要放弃列表,这样我们就可以添加链接而
dropdown-link-container
无需编写无效的 HTML。为了帮助大家理解我的意思,我在这里向一位好心的评论者解释了这个问题! - 你会注意到我们的
dropdown-link-container
元素,它包裹了除徽标之外div
的所有链接。这对于桌面用户来说没什么特别的。但是一旦我们到达移动断点,我们就会将这些元素隐藏在由mobile-dropdown-toggle
按钮触发的大型下拉菜单中。 - 我们正在
aria-hidden
为下拉切换按钮添加一个属性。对于像这样的简单导航,屏幕阅读器没有必要识别这个按钮;即使所有链接“在视觉上隐藏”,它也能识别,所以切换操作根本不会发生。🤷♀️ 不过,如果你真的想为这些用户模拟“切换”效果(对于非常繁忙的导航栏,你应该这样做),你可以考虑aria-expanded
在你的标记中添加这个属性。不过,这篇文章的内容有点杂乱,所以你现在可以使用我的简易方法。
对于那些在家里跟随的人来说,你应该有这样的东西:
现在,一些 CSS
在担心所有移动功能之前,让我们先改善一下宽屏体验。
我们的基本风格
首先,我们将设置导航栏的对齐方式和宽度。
nav {
max-width: 1200px; /* should match the width of your website content */
display: flex;
align-items: center; /* center each of our links vertically */
margin: auto; /* center all our content horizontally when we exceed that max-width */
}
.logo {
margin-right: auto; /* push all our links to the right side, leaving the logo on the left */
}
.dropdown-link-container > a {
margin-left: 20px; /* space out all our links */
}
.mobile-dropdown-toggle {
display: none; /* hide our hamburger button until we're on mobile */
}
这里,这个max-width
属性非常重要。如果没有它,我们的导航链接在大屏幕上就会被推到很靠右的位置(我们的 logo也会被推到很靠左的位置)。下面是一些前后对比,来解释一下我的意思。
*改进前:*我们的导航元素紧贴屏幕边缘。这与页面内容不太匹配,并且在较大的设备上导航时也显得不方便。
*之后:*所有内容都排列整齐,使我们的网站更易于浏览。
当然,你可以根据自己的喜好添加填充、边距和背景颜色👨🍳。但只要你有一个用于将导航栏居中放置的按钮,就已经完成了90%!下面是另一支笔,可以看看它的实际效果max-width
:margin: auto
添加下拉菜单
好了,现在我们来处理下拉菜单的体验。首先,我们只需将链接重新设计成垂直列,使其占据页面的高度:
@media (max-width: 768px) { /* arbitrary breakpoint, around the size of a tablet */
.dropdown-link-container {
/* first, make our dropdown cover the screen */
position: fixed;
top: 0;
left: 0;
right: 0;
height: 100vh;
/* fix nav height on mobile safari, where 100vh is a little off */
height: -webkit-fill-available;
/* then, arrange our links top to bottom */
display: flex;
flex-direction: column;
/* center links vertically, push to the right horizontally.
this means our links will line up with the rightward hamburger button */
justify-content: center;
align-items: flex-end;
/* add margins and padding to taste */
margin: 0;
padding-left: 7vw;
padding-right: 7vw;
background: lightblue;
}
}
在大多数情况下,这是相当标准的。这里仅需要注意几点:
首先,我们使用来将下拉菜单与视口position: fixed
顶部对齐。这与 不同position: absolute
,后者会根据滚动位置移动导航的位置😬
然后,我们使用该-webkit-fill-available
属性在移动版 Safari 中修复导航高度。我敢肯定,你肯定会想:“什么?100vh 怎么就不是用户屏幕尺寸的 100% 呢?苹果这次又搞什么名堂了?”好吧,问题出在 iOS 的地址栏消失机制上。当你滚动页面时,一堆 UI 元素会滑开,为你提供更多的屏幕空间。这当然很棒,但这意味着所有之前占据 100% 屏幕的元素现在都需要调整大小!我们的Bits of Good 非营利组织主页就遇到了这个问题:
注意,直到我们滑动掉所有 Safari 按钮后,链接才会完全垂直居中。如果你有很多链接,这可能会导致文本和图片被截断!
最后,你只需要重写一下height: -webkit-fill-available
来专门解决这个问题。没错,像这样的功能标记-webkit
通常不太受欢迎。但由于这个问题只出现在移动版 Safari(一个 Webkit 浏览器)中,所以在我看来,这种方法其实没什么问题🤷♀️ 最坏的情况是,浏览器会回退到100vh
,这仍然是一个完全可用的体验。
最后,让我们确保我们的logo和下拉按钮确实显示在下拉菜单的顶部。由于position:fixed
,下拉菜单会自然地隐藏其下方的所有内容,直到我们添加一些z-index
内容:
@media (max-width: 768px) {
.logo, .mobile-dropdown-toggle {
z-index: 1;
}
.mobile-dropdown-toggle {
display: initial; /* override that display: none attribute from before */
}
.dropdown-link-container {
...
z-index: 0; /* we're gonna avoid using -1 here, since it could position our navbar below other content on the page as well! */
}
}
将此 CodePen 压缩到我们的断点大小以查看这些样式的实际效果:
让我们为下拉菜单添加动画
好了,大部分标记和样式都完成了。现在,让我们给汉堡按钮加点东西吧!
我们将从处理菜单按钮点击开始。为了向您展示此设置有多么简单,我将仅使用原生 JS:
// get a ref to our navbar (assuming it has this id)
const navElement = document.getElementById("main-nav");
document.addEventListener("click", (event) => {
if (event.target.classList.contains("mobile-dropdown-toggle")) {
// when we click our button, toggle a CSS class!
navElement.classList.toggle("dropdown-opened");
}
});
dropdown-opened
现在,只要应用该类,我们就会让下拉菜单动起来:
/* inside the same media query from before */
@media (max-width: 768px) {
...
.dropdown-link-container {
...
/* our initial state */
opacity: 0; /* fade out */
transform: translateY(-100%); /* move out of view */
transition: transform 0.2s, opacity 0.2s; /* transition these smoothly */
}
nav.dropdown-opened > .dropdown-link-container {
opacity: 1; /* fade in */
transform: translateY(0); /* move into view */
}
}
太棒了!只需几行 CSS,我们就定义了一个点击下拉菜单时淡入淡出 + 滑动的效果。你可以在这里随意调整。随意修改过渡效果!
适应大男孩组件
好吧,我知道你们有些人现在想把它放进你们选择的框架里。嗯,这应该不太难!你可以保留所有 CSS 不变,但这里有一个可以放进 React 的组件代码片段:
export const BigBoyNav = () => {
const [mobileNavOpened, setMobileNavOpened] = useState(false);
const toggleMobileNav = () => setMobileNavOpened(!mobileNavOpened);
return (
<nav className={mobileNavOpened ? 'dropdown-opened' : ''}>
...
<button class="mobile-dropdown-toggle" onClick={toggleMobileNav} aria-hidden="true">
</nav>
)
}
还有一个是针对 Svelte 的:
<!-- ...might've included this to show how simple Svelte is :) -->
<script>
let mobileNavOpened = false
const toggleMobileNav = () => mobileNavOpened = !mobileNavOpened;
</script>
<nav className:mobileNavOpened="dropdown-opened">
...
<button class="mobile-dropdown-toggle" on:click={toggleMobileNav} aria-hidden="true">
</nav>
……你懂的。这是一个开关😆
小事
至此,我们已经有了一个相当不错的 MVP!我只留下了一些辅助功能,希望能够帮助大家顺利到达终点🏁
单击链接时折叠下拉菜单
注意:如果您使用的是 Jekyll、Hugo 或一些纯 HTML 等原生解决方案,则可以跳过此步骤。在这些情况下,点击链接时整个页面都会重新加载,因此无需隐藏下拉菜单!
如果我们要覆盖用户的整个屏幕,我们可能应该在他们选择所需链接后再次隐藏该下拉菜单。我们可以在下拉菜单中添加任何点击事件,如下所示:
document.addEventListener('click', event => {
// if we clicked on something inside our dropdown...
if (ourDropdownElement.contains(event.target)) {
navElement.classList.remove('dropdown-opened')
}
})
……但这不太好用😓。当然,它可以处理鼠标点击,但与使用“Tab”键的键盘导航相比,它又如何呢?那样的话,用户就会跳转到他们想要的链接,按下“Enter”,然后就卡在那里,dropdown-opened
没有任何反馈!
幸运的是,有一种更“声明式”的方法可以解决这个问题。我们不用监听用户点击,而是监听路由变化!这样,我们就不需要考虑用户如何浏览下拉链接了;只需监听结果即可。
当然,这个解决方案取决于你选择的路由器。让我们看看NextJS如何处理这个问题:
export const BigBoyNav = () => {
const router = useRouter(); // grab the current route with a React hook
const activeRoute = router.pathname;
...
// whenever "activeRoute" changes, hide our dropdown
useEffect(() => {
setMobileNavOpened(false);
}, [activeRoute]);
}
原生React Router应该以同样的方式处理这个问题。无论你使用哪种框架,只要确保在活动路由发生变化时触发状态变化即可。
处理“退出”键
为了更好地实现键盘访问,我们还应该在按下“Esc”键时切换下拉菜单。这绑定到非常具体的用户交互,因此我们可以为此添加一个事件监听器:
// vanilla JS
const escapeKeyListener = (event: KeyboardEvent) =>
event.key === 'Escape' && navElement.classList.remove('dropdown-opened')
document.addEventListener('keypress', escapeKeyListener);
...对于组件框架,请确保在组件被销毁时删除该事件监听器:
// React
useEffect(() => {
const escapeKeyListener = (event: KeyboardEvent) =>
event.key === 'Escape' && setMobileNavOpened(false);
// add the listener "on mount"
document.addEventListener('keypress', escapeKeyListener);
// remove the listener "on destroy"
return () => document.removeEventListener('keypress', escapeKeyListener);
}, []);
查看功能齐全的 React 示例🚀
如果您好奇如何在 React 应用程序中将所有这些结合在一起,我们的整个Hack4Impact网站都可以在 CodeSandbox 上访问!
要查看 Nav 组件,请前往此处。
学到一点东西吗?
太棒了!万一你错过了,我特意开通了“网络魔法”简报,来探索更多类似的知识!
这东西探讨的是Web 开发的“首要原则”。换句话说,究竟是哪些糟糕的浏览器 API、扭曲的 CSS 规则以及半无障碍的 HTML 支撑着我们所有的 Web 项目?如果你想要超越框架,那么亲爱的 Web 魔法师,这东西就是为你准备的🔮
赶紧在这里订阅吧!我保证永远教书,绝不会发垃圾信息❤️
文章来源:https://dev.to/hack4impact/building-a-sexy-mobile-ready-navbar-in-any-web-framework-3lm2