原文: How to Build a Responsive Navigation Bar with a Dropdown Menu using JavaScript
导航栏是网站和 web 应用中经常使用的基本组件。作为一名 web 开发人员,你需要能够为客户的项目或者基本的作品集网站定制它。
在本指南中,你将学习如何只用 HTML、CSS 和 JavaScript 从头开始创建一个导航栏。你还将学习如何从无障碍方面优化它。
下面是这个导航栏的截图:
这个设计的灵感来自 Dribbble 上 Tran Mau Tri Tam 的极简导航栏。
第 1 步 - 添加 HTML 标记
为了简洁起见,我们将使用一个叫做 boxicons 的图标库来为这个导航栏导入某些图标。我强烈建议使用内联 SVG。
要使用这个库,请在你的 HTML 文件的 head 插入下面的片段:
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
该标记被分为三个主要部分:
- 一个 class 为
nav-start
的div
元素 - 另一个
div
元素,class 为nav-end
- 一个 id 为
hamburger
的button
元素
所有这些元素都将被包裹在一个 header
标签中。为了更好地解释这一点,请复制下面的标记。
<header id="nav-menu" aria-label="navigation bar">
<div class="container">
<div class="nav-start">
<a class="logo" href="/">
<img src="https://github.com/Evavic44/responsive-navbar-with-dropdown/blob/main/assets/images/logo.png?raw=true"
width="35"
height="35"
alt="Inc Logo"
/>
</a>
<nav class="menu"></nav>
</div>
<div class="nav-end">
<div class="right-container">
<form class="search" role="search">
<input type="search" name="search" placeholder="Search" />
<i class="bx bx-search" aria-hidden="true"></i>
</form>
<a href="#profile">
<img src="https://github.com/Evavic44/responsive-navbar-with-dropdown/blob/main/assets/images/user.jpg?raw=true"
width="30"
height="30"
alt="user image"
/>
</a>
<button class="btn btn-primary">Create</button>
</div>
<button id="hamburger" aria-label="hamburger" aria-haspopup="true" aria-expanded="false">
<i class="bx bx-menu" aria-hidden="true"></i>
</button>
</div>
</div>
</header>
对于 nav-start,我们有以下元素:
- 一个
<img>
元素作为 logo,包裹在一个<a>
标签中 - 一个
<nav>
元素,class 为menu
,包含所有导航链接,我们将使用<ul>
、<li>
和<a>
定义这些链接
nav-end 有以下元素:
- 一个
<form>
元素,它的 role 是 search,包含一个搜索输入和搜索图标 - 一个 class 为
btn
的按钮元素,我们将使用这个 class 来设计按钮
对于汉堡包按钮:
- 一个按钮,其 id 和
aria-label
为 hamburger,aria-haspopup
设置为 “true”,aria-expanded
设置为 “false”。这些标签将使我们能够使这个按钮更容易被屏幕阅读器访问。
下面是输出:
导航菜单
导航菜单 <nav>
是导航链接所在的位置。用下面这个标记替换你先前添加的 nav
元素:
<nav class="menu">
<ul class="menu-bar">
<li>
<button
class="nav-link dropdown-btn"
data-dropdown="dropdown1"
aria-haspopup="true"
aria-expanded="false"
aria-label="browse">
Browse
<i class="bx bx-chevron-down" aria-hidden="true"></i>
</button>
<div id="dropdown1" class="dropdown"></div>
</li>
<li>
<button
class="nav-link dropdown-btn"
data-dropdown="dropdown2"
aria-haspopup="true"
aria-expanded="false"
aria-label="discover">
Discover
<i class="bx bx-chevron-down" aria-hidden="true"></i>
</button>
<div id="dropdown2" class="dropdown"></div>
</li>
<li><a class="nav-link" href="/">Jobs</a></li>
<li><a class="nav-link" href="/">Livestream</a></li>
<li><a class="nav-link" href="/">About</a></li>
</ul>
</nav>
这里你有一个 nav
标签,它包含一个无序的列表,其中有五个 li
元素,代表每个导航菜单项目:browse、discover、 jobs、livestream 和 about。
前两个元素,browse 和 discover,是 button
元素,将被用来切换它们各自的下拉菜单。而元素 Jobs、livestream 和 about 只是普通的链接。
使用到目前为止的代码,你的结果应该是这样的:
下拉元素
接下来,让我们为每个导航按钮定义下拉元素。下面是第一个下拉菜单的标记。把你的标记中的第一个 li
元素替换成这样:
<!-- markup truncated for brevity-->
<li>
<button
class="nav-link dropdown-btn"
data-dropdown="dropdown1"
aria-haspopup="true"
aria-expanded="false"
aria-label="browse"
>
Browse
<i class="bx bx-chevron-down" aria-hidden="true"></i>
</button>
<div id="dropdown1" class="dropdown">
<ul role="menu">
<li role="menuitem">
<a class="dropdown-link" href="#best-of-the-day">
<img src="./assets/icons/botd.svg" class="icon" />
<div>
<span class="dropdown-link-title"
>Best of the day</span
>
<p>Shorts featured today by curators</p>
</div>
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#featured-streams">
<img src="./assets/icons/fs.svg" class="icon" />
<div>
<span class="dropdown-link-title"
>Featured Streams</span
>
<p>Leading creatives livestreams</p>
</div>
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#subscriptions">
<img src="./assets/icons/sp.svg" class="icon" />
<div>
<span class="dropdown-link-title">Subscriptions</span>
<p>Gain exclusive access</p>
</div>
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#creative-feed">
<img src="./assets/icons/cf.svg" class="icon" />
<div>
<span class="dropdown-link-title">Creative Feed</span>
<p>See trending creations</p>
</div>
</a>
</li>
</ul>
<ul role="menu">
<li class="dropdown-title">
<span class="dropdown-link-title">Browse by apps</span>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#adobe-xd">
<img src="./assets/icons/xd.svg" />
Adobe XD
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#after-effect">
<img src="./assets/icons/ae.svg" />
After Effect
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#sketch">
<img src="./assets/icons/sketch.svg" />
Sketch
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#indesign">
<img src="./assets/icons/indesign.svg" />
Indesign
</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#figma">
<img src="./assets/icons/figma.svg" />
Figma
</a>
</li>
</ul>
</div>
</li>
你可以在这里获得 SVG 图标。
看看这个标记,我们添加了以下内容:
- 一个 id 为
dropdown1
、class 为dropdown
的div
元素。 - 两个
ul
元素,每个元素的 role 是"menu"
。 - 一个
span
元素,其 class 为dropdown-link-title
,作为每个menu
集合的 header。 - 使用
li
和a
标签定义的链接集合,li
标签有一个 role 为"menuitem"
,每个链接有一个 class 为dropdown-link
。 - 在每个锚标签内,通过
img
标签添加一个图标。
注意:由于通过img标签添加的图标是严格意义上的声明性的,我强烈建议你直接将它们作为SVG元素添加。我这样做只是为了使代码更容易阅读
下面是第二个下拉元素dropdown2的标记:
<!-- markup truncated for brevity-->
<li>
<button
class="nav-link dropdown-btn"
data-dropdown="dropdown2"
aria-haspopup="true"
aria-expanded="false"
aria-label="discover"
>
Discover
<i class="bx bx-chevron-down" aria-hidden="true"></i>
</button>
<div id="dropdown2" class="dropdown">
<ul role="menu">
<li>
<span class="dropdown-link-title">Browse Categories</span>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#branding">Branding</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#illustrations">Illustration</a>
</li>
</ul>
<ul role="menu">
<li>
<span class="dropdown-link-title">Download App</span>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#mac-windows">MacOS & Windows</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#linux">Linux</a>
</li>
</ul>
</div>
</li>
最终结果应该是这样:
本教程最后将提供完整的标记。
第 2 步 - 为导航栏设计样式
像往常一样,我们将从重置页面上每个元素的默认 margin 和 padding 开始,添加全局变量,并对一些元素进行一些基本的样式设计。
/* style.css */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Inter", sans-serif;
}
:root {
--dark-grey: #333333;
--medium-grey: #636363;
--light-grey: #eeeeee;
--ash: #f4f4f4;
--primary-color: #2b72fb;
--white: white;
--border: 1px solid var(--light-grey);
--shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px,
rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
}
body {
font-family: inherit;
background-color: var(--white);
color: var(--dark-grey);
letter-spacing: -0.4px;
}
ul {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
button {
border: none;
background-color: transparent;
cursor: pointer;
color: inherit;
}
接下来,添加一些可重复使用的样式。
.btn {
display: block;
background-color: var(--primary-color);
color: var(--white);
text-align: center;
padding: 0.6rem 1.4rem;
font-size: 1rem;
font-weight: 500;
border-radius: 5px;
}
.icon {
padding: 0.5rem;
background-color: var(--light-grey);
border-radius: 10px;
}
.logo {
margin-right: 1.5rem;
}
#nav-menu {
border-bottom: var(--border);
}
.container {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1600px;
margin: 0 auto;
column-gap: 2rem;
height: 90px;
padding: 1.2rem 3rem;
}
现在你已经得到了这些基本的样式,你可以专注于核心导航栏本身的样式。
导航菜单的样式
下面是为导航栏容器设计的标记:
.menu {
position: relative;
background: var(--white);
}
.menu-bar li:first-child .dropdown {
flex-direction: initial;
min-width: 480px;
}
.menu-bar li:first-child ul:nth-child(1) {
border-right: var(--border);
}
.menu-bar li:nth-child(n + 2) ul:nth-child(1) {
border-bottom: var(--border);
}
.menu-bar .dropdown-link-title {
font-weight: 600;
}
.menu-bar .nav-link {
font-size: 1rem;
font-weight: 500;
letter-spacing: -0.6px;
padding: 0.3rem;
min-width: 60px;
margin: 0 0.6rem;
}
.menu-bar .nav-link:hover,
.dropdown-link:hover {
color: var(--primary-color);
}
.nav-start,
.nav-end,
.menu-bar,
.right-container,
.right-container .search {
display: flex;
align-items: center;
}
下拉菜单的样式
除了对下拉菜单进行样式设计外,还将使用 visibility
和 opacity
属性的组合来隐藏它。我们的想法是,只有在一个按钮被点击时才显示菜单。
.dropdown {
display: flex;
flex-direction: column;
min-width: 230px;
background-color: var(--white);
border-radius: 10px;
position: absolute;
top: 36px;
z-index: 1;
visibility: hidden;
opacity: 0;
transform: scale(0.97) translateX(-5px);
transition: 0.1s ease-in-out;
box-shadow: var(--shadow);
}
.dropdown.active {
visibility: visible;
opacity: 1;
transform: scale(1) translateX(5px);
}
.dropdown ul {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.2rem;
font-size: 0.95rem;
}
.dropdown-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.15rem;
}
.dropdown-link {
display: flex;
gap: 0.5rem;
padding: 0.5rem 0;
border-radius: 7px;
transition: 0.1s ease-in-out;
}
.dropdown-link p {
font-size: 0.8rem;
color: var(--medium-grey);
}
接着,可以通过使用 active
class 将 visibility
和 opacity
属性恢复到默认状态来切换该菜单。但我们将通过 JavaScript 来做这件事。
如果你喜欢完全隐藏菜单,可以用 display: none;
代替 opacity
和 visibility
属性。虽然这个属性在 CSS 中不能用过渡来做动画。
右边的菜单样式
接下来,为搜索输入、按钮和个人资料图片添加样式,然后在桌面屏幕上隐藏汉堡包按钮。
.right-container {
display: flex;
align-items: center;
column-gap: 1rem;
}
.right-container .search {
position: relative;
}
.right-container img {
border-radius: 50%;
}
.search input {
background-color: var(--ash);
border: none;
border-radius: 6px;
padding: 0.7rem;
padding-left: 2.4rem;
font-size: 16px;
width: 100%;
border: var(--border);
}
.search .search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
opacity: 0.6;
}
#hamburger {
display: none;
padding: 0.1rem;
margin-left: 1rem;
font-size: 1.9rem;
}
现在它是这样的:
为了完成样式设计,添加媒体查询样式:
@media (max-width: 1100px) {
#hamburger {
display: block;
}
.container {
padding: 1.2rem;
}
.menu {
display: none;
position: absolute;
top: 87px;
left: 0;
min-height: 100vh;
width: 100vw;
}
.menu-bar li:first-child ul:nth-child(1) {
border-right: none;
border-bottom: var(--border);
}
.dropdown {
display: none;
min-width: 100%;
border: none !important;
border-radius: 5px;
position: static;
top: 0;
left: 0;
visibility: visible;
opacity: 1;
transform: none;
box-shadow: none;
}
.menu.show,
.dropdown.active {
display: block;
}
.dropdown ul {
padding-left: 0.3rem;
}
.menu-bar {
display: flex;
flex-direction: column;
align-items: stretch;
row-gap: 1rem;
padding: 1rem;
}
.menu-bar .nav-link {
display: flex;
justify-content: space-between;
width: 100%;
font-weight: 600;
font-size: 1.2rem;
margin: 0;
}
.menu-bar > li:not(:last-child) {
padding-bottom: 0.5rem;
border-bottom: var(--border);
}
}
@media (max-width: 600px) {
.right-container {
display: none;
}
}
首先,这排列了元素,最重要的是,它定位 hamburger
class 并将其隐藏。现在在平板电脑和手机屏幕上,导航栏是响应式的,汉堡包按钮是可见的。
这就完成了导航栏的样式设计。让我们在下一节中进行功能设计。
第 3 步 - 添加 JavaScript 功能
对于 JavaScript 功能,我们将专注于以下几个类别:
- 切换下拉菜单的可见性
- 关闭下拉菜单
- 切换汉堡包菜单的可见性
- 切换 aria-expanded 属性
首先,使用 DOM 的 querySelector
方法选择你的类,并将它们存储在变量中,以便它们可以重复使用。
// script.js
const dropdownBtn = document.querySelectorAll(".dropdown-btn");
const dropdown = document.querySelectorAll(".dropdown");
const hamburgerBtn = document.getElementById("hamburger");
const navMenu = document.querySelector(".menu");
const links = document.querySelectorAll(".dropdown a");
接下来在你的代码中添加下面的函数。我稍后将解释它们的用途。
function setAriaExpandedFalse() {
dropdownBtn.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
}
function closeDropdownMenu() {
dropdown.forEach((drop) => {
drop.classList.remove("active");
drop.addEventListener("click", (e) => e.stopPropagation());
});
}
function toggleHamburger() {
navMenu.classList.toggle("show");
}
获取下拉菜单 ID
下一步是获取下拉菜单的 ID。由于有两个下拉菜单,其值将基于点击的下拉按钮。
为了获得 ID,你将利用 dataset
属性,然后将该值存储到它自己的变量中。
dropdownBtn.forEach((btn) => {
btn.addEventListener("click", function (e) {
const dropdownIndex = e.currentTarget.dataset.dropdown;
const dropdownElement = document.getElementById(dropdownIndex);
console.log(dropdownElement);
});
});
理解这个片段:
forEach
方法遍历按钮的集合addEventListener()
方法为每个按钮附加了一个点击事件currentTarget.dataset
属性获取被点击按钮的当前下拉菜单- 每一个 id 都被用来定位相应的下拉元素
这意味着,当 dataset 为 dropdown1
的按钮被点击时,id 为 dropdown1
的 div
元素被记录到控制台,反之则为 dropdown2
按钮。
切换下拉菜单
切换菜单是相当容易的,因为你已经把下拉元素的 ID 存储到一个叫作 dropdownElement
的变量中。通过定位这个变量,你可以切换每个下拉元素的 active
class。
dropdownBtn.forEach((btn) => {
btn.addEventListener("click", function (e) {
const dropdownIndex = e.currentTarget.dataset.dropdown;
const dropdownElement = document.getElementById(dropdownIndex);
dropdownElement.classList.toggle("active");
dropdown.forEach((drop) => {
if (drop.id !== btn.dataset["dropdown"]) {
drop.classList.remove("active");
}
});
e.stopPropagation();
});
});
除了切换下拉菜单外,我们还添加了一个条件,检查当前下拉元素的 id 是否与活动按钮相匹配。这可以确保每次只有一个下拉元素被展开。
切换 aria-expanded 属性
aria-expanded
属性允许辅助技术告知一个交互式菜单是展开还是折叠的。要切换这个属性,请在 btn
代码块中的 e.stopPropagation()
下插入这段代码:
btn.setAttribute(
"aria-expanded",
btn.getAttribute("aria-expanded") === "false" ? "true" : "false"
);
现在,只要下拉菜单是可见的,aria-expanded
属性就被设置为 true;而当菜单折叠时,它被设置为 false。
折叠下拉菜单
到目前为止,下拉菜单只有在点击按钮的时候才会折叠。它应该被折叠的其他情况包括:
- 当点击下拉菜单内的链接时
- 当你按下 ESC 键时
- 当你点击文档主体时——在下拉容器之外
通过调用前面创建的函数 closeDropdownMenu
和 setAriaExpandedFalse
,可以折叠下拉菜单并将 aria-expanded
属性设置为 false。
// 当点击下拉链接时关闭下拉菜单
links.forEach((link) =>
link.addEventListener("click", () => {
closeDropdownMenu();
setAriaExpandedFalse();
})
);
// 当点击文档主体时关闭下拉菜单
document.documentElement.addEventListener("click", () => {
closeDropdownMenu();
setAriaExpandedFalse();
});
// 当按 ESC 键时关闭下拉菜单
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeDropdownMenu();
setAriaExpandedFalse();
}
});
下面是输出:
切换汉堡包菜单
要在平板电脑和手机屏幕上看到导航栏,请将 toggleHamburger
函数作为汉堡包按钮的回调,然后在 links
代码块内调用该函数。
links.forEach((link) =>
link.addEventListener("click", () => {
closeDropdownMenu();
setAriaExpandedFalse();
toggleHamburger();
})
);
hamburgerBtn.addEventListener("click", toggleHamburger);
这会切换一个名为 show
的不同 class,控制显示或隐藏导航栏。
下面是最终的输出:
添加更多下拉菜单
你可以添加更多下拉菜单,只需将任何一个列表项替换为按钮和下拉菜单的链接。为了使其发挥作用,请确保你更新以下内容:
- 根据你需要的菜单数量,更新下拉菜单的 ID。例如,第三个菜单的 ID 是
dropdown3
- 按钮的
data-dropdown
值将设置为dropdown3
下面是一个将 Jobs 链接转换为下拉菜单的例子。
之前:
<li><a class="nav-link" href="/">Jobs</a></li>
之后:
<li>
<button
class="nav-link dropdown-btn"
data-dropdown="dropdown3"
aria-haspopup="true"
aria-expanded="false"
aria-label="jobs"
>
Jobs
<i class="bx bx-chevron-down" aria-hidden="true"></i>
</button>
<div id="dropdown3" class="dropdown">
<ul role="menu">
<li><span class="dropdown-link-title">Software</span></li>
<li role="menuitem">
<a class="dropdown-link" href="#frontend">Frontend</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#backend">Backend</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#ai-ml">AI/ML</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#mobile-dev">Mobile Development</a>
</li>
</ul>
<ul role="menu">
<li>
<span class="dropdown-link-title">Others</span>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#ui-ux">UI/UX</a>
</li>
<li role="menuitem">
<a class="dropdown-link" href="#writing">Technical Writing</a>
</li>
</ul>
</div>
</li>
这是最终的结果:
按照这个过程,你可以添加你想要的下拉菜单。
就这样,你用 HTML、CSS 和 JavaScript 成功地创建了一个带有下拉菜单的响应式导航栏。你还学会了如何使用包括 aria-expanded
属性在内的几个 aria 属性来使菜单可访问。
下面是测试这个导航栏运行情况的 CodePen 文件:
这是 GitHub 的代码链接。
总结
我真诚地希望你觉得这篇文章有趣或有用。如果你这么想,请与你的朋友分享它或订阅我的博客,这样你就不会错过任何未来的文章。感谢阅读。