原文: How to Build a Responsive Navigation Bar with a Dropdown Menu using JavaScript

导航栏是网站和 web 应用中经常使用的基本组件。作为一名 web 开发人员,你需要能够为客户的项目或者基本的作品集网站定制它。

在本指南中,你将学习如何只用 HTML、CSS 和 JavaScript 从头开始创建一个导航栏。你还将学习如何从无障碍方面优化它。

下面是这个导航栏的截图:

navigation-bar-final-result

这个设计的灵感来自 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-startdiv 元素
  • 另一个 div 元素,class 为 nav-end
  • 一个 id 为 hamburgerbutton 元素

所有这些元素都将被包裹在一个 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”。这些标签将使我们能够使这个按钮更容易被屏幕阅读器访问。

下面是输出:

markup-elements-broken-down-into-three-main-parts-1

导航菜单

导航菜单 <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 元素,代表每个导航菜单项目:browsediscover jobslivestream about

前两个元素,browse 和 discover,是 button 元素,将被用来切换它们各自的下拉菜单。而元素 Jobs、livestream 和 about 只是普通的链接。

使用到目前为止的代码,你的结果应该是这样的:

Navigation-markup-with-links-and-popup-buttons

下拉元素

接下来,让我们为每个导航按钮定义下拉元素。下面是第一个下拉菜单的标记。把你的标记中的第一个 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 为 dropdowndiv 元素。
  • 两个 ul 元素,每个元素的 role 是 "menu"
  • 一个 span 元素,其 class 为 dropdown-link-title,作为每个 menu 集合的 header。
  • 使用 lia 标签定义的链接集合,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>

最终结果应该是这样:

popup1-and-popup2-markup-1

本教程最后将提供完整的标记。

第 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;
}

下拉菜单的样式

除了对下拉菜单进行样式设计外,还将使用 visibilityopacity 属性的组合来隐藏它。我们的想法是,只有在一个按钮被点击时才显示菜单。

.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 将 visibilityopacity 属性恢复到默认状态来切换该菜单。但我们将通过 JavaScript 来做这件事。

如果你喜欢完全隐藏菜单,可以用 display: none; 代替 opacityvisibility 属性。虽然这个属性在 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;
}

现在它是这样的:

final-styling-output-of-navigation-bar-and-popup-menu

为了完成样式设计,添加媒体查询样式:

@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 并将其隐藏。现在在平板电脑和手机屏幕上,导航栏是响应式的,汉堡包按钮是可见的。

responsive-navigation-bar-2

这就完成了导航栏的样式设计。让我们在下一节中进行功能设计。

第 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 为 dropdown1div 元素被记录到控制台,反之则为 dropdown2 按钮。

get-popup-element-id-dynamically-1
使用按钮 dataset 属性动态地获得每个下拉元素

切换下拉菜单

切换菜单是相当容易的,因为你已经把下拉元素的 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 是否与活动按钮相匹配。这可以确保每次只有一个下拉元素被展开。

toggling-dropdown-element
切换下拉菜单

切换 aria-expanded 属性

aria-expanded 属性允许辅助技术告知一个交互式菜单是展开还是折叠的。要切换这个属性,请在 btn 代码块中的 e.stopPropagation() 下插入这段代码:

btn.setAttribute(
    "aria-expanded",
    btn.getAttribute("aria-expanded") === "false" ? "true" : "false"
);

现在,只要下拉菜单是可见的,aria-expanded 属性就被设置为 true;而当菜单折叠时,它被设置为 false。

toggling-the-aria-expanded-property
切换 aria-expanded 属性

折叠下拉菜单

到目前为止,下拉菜单只有在点击按钮的时候才会折叠。它应该被折叠的其他情况包括:

  • 当点击下拉菜单内的链接时
  • 当你按下 ESC 键时
  • 当你点击文档主体时——在下拉容器之外

通过调用前面创建的函数 closeDropdownMenusetAriaExpandedFalse,可以折叠下拉菜单并将 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();
  }
});

下面是输出:

closing-dropdown-menu-when-dropdown-links-and-escape-key-is-clicked

切换汉堡包菜单

要在平板电脑和手机屏幕上看到导航栏,请将 toggleHamburger 函数作为汉堡包按钮的回调,然后在 links 代码块内调用该函数。

links.forEach((link) =>
  link.addEventListener("click", () => {
    closeDropdownMenu();
    setAriaExpandedFalse();
    toggleHamburger();
  })
);
hamburgerBtn.addEventListener("click", toggleHamburger);

这会切换一个名为 show 的不同 class,控制显示或隐藏导航栏。

下面是最终的输出:

show-hamburger-menu-on-tablet-and-mobile-screens

添加更多下拉菜单

你可以添加更多下拉菜单,只需将任何一个列表项替换为按钮和下拉菜单的链接。为了使其发挥作用,请确保你更新以下内容:

  • 根据你需要的菜单数量,更新下拉菜单的 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>

这是最终的结果:

additional-dropdown-menu

按照这个过程,你可以添加你想要的下拉菜单。

就这样,你用 HTML、CSS 和 JavaScript 成功地创建了一个带有下拉菜单的响应式导航栏。你还学会了如何使用包括 aria-expanded 属性在内的几个 aria 属性来使菜单可访问。

下面是测试这个导航栏运行情况的 CodePen 文件:

这是 GitHub 的代码链接

总结

我真诚地希望你觉得这篇文章有趣或有用。如果你这么想,请与你的朋友分享它或订阅我的博客,这样你就不会错过任何未来的文章。感谢阅读。

GitHub | Twitter | Blog | LinkedIn