导航菜单代码示例:制作带下拉子菜单的网页导航

AI教程13小时前发布 jdkjadmin
0 00
导航菜单代码示例:制作带下拉子菜单的网页导航

导航菜单代码示例:制作带下拉子菜单的网页导航

下拉导航适合把网站的主要栏目和相关子栏目组织在一起:例如企业站的“产品”与产品分类、文档站的“指南”与章节入口,或电商网站的商品类别。它能节省页面空间,但也容易引入键盘无法操作、触屏上难以展开、菜单被容器裁切等问题。

本文用原生 HTML 和 CSS 实现一个可直接运行的导航示例。菜单使用 <nav>、列表、链接以及原生的 <details> 和 <summary>。不依赖框架或 JavaScript,鼠标、触屏和键盘用户都能展开子菜单。示例适合作为普通网站导航的起点;如果要实现复杂的应用菜单、路由状态同步或精细的焦点管理,则需要进一步设计交互。

一、什么时候适合使用下拉子菜单

下拉菜单适用于栏目数量较多,但用户仍能根据清晰的类别名称找到目标的场景。顶层菜单应表达网站的主要分区,子菜单则补充该分区下的页面。例如“产品”是顶层入口,子菜单可以列出“产品概览”“产品 A”“产品 B”。

如果网站只有少量页面,把每个页面直接放在导航栏中通常更简单。如果用户必须逐层展开才能理解网站结构,或者菜单包含很多层级,下拉菜单反而会增加寻找成本。一般应尽量减少层级和子项数量,并考虑在栏目页提供完整的分类入口。

需要区分“导航”与“应用菜单”。网站导航用于跳转页面,适合用链接表达;应用菜单则可能包含按钮、勾选项、快捷键和复杂键盘行为。不要仅仅因为视觉上像菜单,就把网页导航实现成依赖方向键、角色属性和自定义焦点管理的 ARIA 菜单。语义和交互应与真实用途相匹配。

二、完整可运行示例

将下面代码保存为 index.html,用浏览器打开即可查看。示例中的链接指向演示地址,请替换成网站实际页面。为了简化演示,菜单没有加入品牌图片,也不需要外部依赖。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>带下拉子菜单的导航示例</title>
  <style>
    :root {
      font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
      color: #172033;
      background: #f5f7fb;
    }

    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
    }

    .site-header {
      background: #fff;
      border-bottom: 1px solid #dce2ec;
    }

    .site-nav {
      max-width: 72rem;
      margin: 0 auto;
      padding: 0.75rem 1.25rem;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1.5rem;
    }

    .brand {
      color: #173b75;
      font-size: 1.15rem;
      font-weight: 750;
      text-decoration: none;
      white-space: nowrap;
    }

    .nav-list,
    .submenu {
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .nav-list {
      display: flex;
      align-items: center;
      gap: 0.35rem;
    }

    .nav-link,
    .menu-summary {
      display: inline-flex;
      align-items: center;
      min-height: 2.75rem;
      padding: 0.55rem 0.8rem;
      border: 0;
      border-radius: 0.45rem;
      background: transparent;
      color: #26334a;
      font: inherit;
      text-decoration: none;
      cursor: pointer;
    }

    .nav-link:hover,
    .menu-summary:hover,
    .nav-link[aria-current="page"] {
      background: #edf3ff;
      color: #164fa4;
    }

    .nav-link:focus-visible,
    .menu-summary:focus-visible,
    .brand:focus-visible {
      outline: 3px solid #2874d0;
      outline-offset: 3px;
    }

    .has-submenu {
      position: relative;
    }

    .menu-summary {
      list-style: none;
      gap: 0.45rem;
    }

    .menu-summary::-webkit-details-marker {
      display: none;
    }

    .menu-summary::after {
      content: "";
      width: 0.45rem;
      height: 0.45rem;
      border-right: 2px solid currentColor;
      border-bottom: 2px solid currentColor;
      transform: translateY(-0.12rem) rotate(45deg);
      transition: transform 160ms ease;
    }

    .has-submenu[open] > .menu-summary::after {
      transform: translateY(0.1rem) rotate(225deg);
    }

    .submenu {
      position: absolute;
      z-index: 10;
      top: calc(100% + 0.2rem);
      left: 0;
      min-width: 13rem;
      padding: 0.4rem;
      border: 1px solid #dce2ec;
      border-radius: 0.65rem;
      background: #fff;
      box-shadow: 0 0.7rem 1.8rem rgb(20 35 60 / 14%);
    }

    .submenu a {
      display: block;
      padding: 0.65rem 0.75rem;
      border-radius: 0.4rem;
      color: #26334a;
      text-decoration: none;
    }

    .submenu a:hover,
    .submenu a:focus-visible {
      background: #edf3ff;
      color: #164fa4;
    }

    main {
      max-width: 72rem;
      margin: 0 auto;
      padding: 3rem 1.25rem;
    }

    main p {
      max-width: 42rem;
      line-height: 1.75;
    }

    @media (max-width: 42rem) {
      .site-nav {
        align-items: flex-start;
        flex-direction: column;
        gap: 0.5rem;
      }

      .nav-list {
        width: 100%;
        align-items: stretch;
        flex-direction: column;
      }

      .nav-list > li {
        width: 100%;
      }

      .nav-link,
      .menu-summary {
        width: 100%;
        justify-content: space-between;
      }

      .submenu {
        position: static;
        min-width: 0;
        margin: 0.15rem 0 0.4rem 0.5rem;
        box-shadow: none;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      *,
      *::before,
      *::after {
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <header class="site-header">
    <nav class="site-nav" aria-label="主导航">
      <a class="brand" href="/">示例网站</a>

      <ul class="nav-list">
        <li>
          <a class="nav-link" href="/" aria-current="page">首页</a>
        </li>

        <li>
          <details class="has-submenu">
            <summary class="menu-summary">产品</summary>
            <ul class="submenu">
              <li><a href="/products/">产品概览</a></li>
              <li><a href="/products/alpha">产品 A</a></li>
              <li><a href="/products/beta">产品 B</a></li>
            </ul>
          </details>
        </li>

        <li>
          <details class="has-submenu">
            <summary class="menu-summary">资源</summary>
            <ul class="submenu">
              <li><a href="/guides/">使用指南</a></li>
              <li><a href="/api/">API 文档</a></li>
              <li><a href="/support/">帮助中心</a></li>
            </ul>
          </details>
        </li>

        <li>
          <a class="nav-link" href="/about">关于我们</a>
        </li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>欢迎访问示例网站</h1>
    <p>点击或使用键盘聚焦“产品”和“资源”,即可展开对应的子菜单。选择子菜单中的链接会跳转到相应页面。</p>
  </main>
</body>
</html>

三、代码中几个关键设计

用链接表达跳转,用 summary 表达展开

顶层的“首页”和“关于我们”是链接,因为它们直接跳转页面。带子菜单的“产品”和“资源”则用 <summary> 作为展开控件,并由 <details> 提供打开、关闭状态。浏览器会为 <summary> 提供原生交互,通常可以用鼠标、触屏和键盘操作,也会向辅助技术暴露展开状态。

这种写法有一个实际取舍:“产品”本身不再是直接跳转链接。如果产品栏目既需要展开子菜单,又需要点击标题进入总览页,不应简单地把链接塞进 <summary>,因为这会让交互和可访问性变得不清晰。可将“产品概览”作为子菜单第一项,或将标题拆分成独立链接和展开按钮,并仔细处理布局、名称和键盘行为。

不只依赖悬停

许多传统菜单只写 :hover,鼠标移入时才显示子菜单。触屏设备没有稳定的悬停行为,键盘用户也无法靠鼠标移动展开菜单。示例依靠原生的 <details> 状态控制显示,因此点击、触摸和键盘操作使用的是同一套交互,不需要重复维护一套 JavaScript 状态。

示例中的 position: absolute 用于桌面端把子菜单放在导航项下方;较窄屏幕下则切换为 position: static,让子菜单进入正常布局流。这能降低菜单覆盖正文、超出屏幕边缘的风险。具体断点应结合实际导航内容和测试结果调整,而不是把某个像素值当作所有网站的通用标准。

保留可见焦点

:focus-visible 为键盘用户提供清晰的焦点轮廓。不要为了追求视觉简洁就写 outline: none 而不提供替代样式。焦点轮廓应与背景有足够区分,并且不能被容器裁切。示例也对链接悬停和当前页面提供背景与颜色提示;正式站点还应验证颜色对比度,并避免只用颜色表达状态。

导航区域要有名称

<nav aria-label="主导航"> 给导航地标提供了可识别的名称。如果页面有多个导航区,例如页头导航和页脚导航,应使用不同且准确的名称,方便屏幕阅读器用户区分。链接文本也要能说明目的地,尽量避免在多个位置重复使用含义不明的“点击这里”。

aria-current="page" 用来表示当前页面对应的导航链接。示例把它放在首页链接上只是演示;实际页面应由模板或前端路由根据当前地址设置到正确的链接上。不要把它固定到首页之外的所有页面,也不要把一个菜单中的每个链接都标成当前页面。

四、适用边界与可访问性检查

原生 <details> 适合折叠说明、简单的展开列表和本例这类轻量导航。在现代主流浏览器中通常可以正常使用,但正式上线仍应按照项目支持的浏览器范围做测试。如果产品必须兼容较老的浏览器、要求点击其他菜单时自动关闭当前菜单,或需要路由切换后关闭菜单,可能需要 JavaScript 来补充行为。

本例没有实现“点击页面其他位置关闭菜单”“按 Escape 关闭所有菜单”以及“打开一个时自动关闭其他菜单”。由于 <details> 是原生控件,浏览器和辅助技术的具体行为会有差异。若产品需求明确要求这些行为,需要在目标浏览器、键盘和屏幕阅读器组合上验证,再谨慎增加脚本;脚本不能只处理鼠标事件,还应维护正确的展开状态、焦点位置和关闭行为。

如果用 JavaScript 自制下拉菜单,至少要确认以下问题:展开控件能通过键盘操作;当前展开状态可被辅助技术识别;关闭菜单后焦点不会丢失到不可预期的位置;菜单项是正常的网页链接;触屏用户可以明确打开和关闭;路由跳转、页面滚动和窗口尺寸变化时状态仍合理。不要仅凭添加 role="menu" 就认为菜单变得可访问。ARIA 角色不会自动提供键盘行为,应用菜单角色通常意味着还要实现相应的方向键、焦点移动等模式。

菜单项也不宜无限增加。窄屏上导航会变长,子菜单可能超出视口;桌面上若菜单紧贴浏览器边缘,也可能被裁切。需要针对较长标签、缩放页面、不同语言和小屏设备做测试。导航内容要能随字体放大而扩展,避免固定宽度和固定高度把文字截断。

五、常见错误及修正思路

错误一:只有鼠标悬停才能打开

这种实现排除了触屏和许多键盘操作方式。应提供可聚焦的展开控件,并让它通过点击或键盘激活。即使保留悬停作为桌面端的辅助行为,也不应把它作为唯一入口。

错误二:把整块标题做成不明确的混合控件

同一个区域既跳转又展开,会让用户难以预测点击结果。明确划分职责:要么标题只展开,子菜单提供总览链接;要么把标题链接和独立的展开按钮分开,并为两个控件提供清晰名称和焦点样式。

错误三:子菜单被父容器裁切

绝对定位的菜单可能受到祖先元素的 overflow: hidden 或滚动容器影响,即使设置很大的 z-index 也无法越过某些裁切边界。排查时检查相关祖先的溢出、定位和堆叠上下文;优先调整布局结构,而不是不断增大层级数值。移动端改用文档流布局通常更稳妥。

错误四:用空链接或错误的链接地址占位

href="#" 通常会改变页面片段位置,容易让页面跳动,也不能作为真实页面地址的替代品。尚未确定目的地时,可以先不要呈现为链接,或在内容模型中明确标记未发布状态;发布前应检查每个菜单项确实有有效目的地。

错误五:只靠颜色区分悬停和当前状态

对比不足的颜色变化可能难以辨认,也不一定能被所有用户感知。除了颜色,可以组合背景、下划线、字重或形状等提示。焦点状态必须清晰,并在实际背景、缩放和高对比度环境下检查。

错误六:为复杂行为随意添加 ARIA

属性不是交互逻辑的替代品。只有在语义确实需要时才添加 ARIA,并确认属性值会随状态变化。对普通网站导航,原生列表、链接和折叠控件通常比手工拼出的角色组合更容易维护。错误或多余的属性反而可能让辅助技术获得互相矛盾的信息。

六、参数、输入和安全边界

示例没有接受用户输入,也没有提供可配置的 JavaScript 参数。max-width、padding 和媒体查询中的宽度属于 CSS 样式值,应根据布局设计调整;它们不是具有安全语义的参数,也不能用于限制菜单链接的访问权限。调整这些数值不会验证地址、过滤用户输入或阻止攻击。

示例中的 href 是静态路径。真实项目中,菜单地址可能来自 CMS、数据库或配置文件。不要把未经验证的用户输入直接拼接到链接地址、HTML 属性或页面内容中。对可能变化的地址,应按照业务规则限制允许的协议和站点;尤其不能默认把任意字符串当成可信 URL。HTML 转义应按输出上下文执行:文本节点、属性值、JavaScript 和 CSS 不是同一种上下文,单纯替换尖括号不能代替完整的安全处理。

本例没有在新标签页打开外部链接,因此不需要设置 target="_blank"。如果业务确实需要新标签页,通常应使用 rel="noopener noreferrer",并考虑告知用户链接会在新窗口或标签页打开。不要把这两个属性误解为 URL 白名单或完整的内容安全策略,它们解决的是特定的链接上下文问题。

隐藏子菜单只是视觉和交互状态,不是访问控制。把某个链接从导航里移除、用 CSS 隐藏,或不给普通用户展示入口,都不能阻止用户直接访问对应地址。涉及权限的数据和页面必须由服务器端或可信的后端服务校验;前端导航只能帮助用户发现可用页面,不能承担授权职责。

同样,aria-label、aria-current 和焦点样式用于改善结构和交互,不提供认证、授权或数据保护。不要把页面上的菜单状态当作安全边界。是否允许访问某个资源,应在每次相关请求中按照应用的身份认证和权限规则判断。

七、上线前检查清单

  • 不使用鼠标时,能否通过键盘找到并展开每个子菜单?焦点是否清楚可见?
  • 触屏设备上,展开控件是否容易点击,展开后是否能看到所有子项?
  • 所有菜单项的链接是否有效,名称是否能说明目的地?当前页面状态是否准确?
  • 浏览器缩放、较长文本、小屏宽度和不同语言下,菜单是否溢出、重叠或被裁切?
  • 是否检查了目标浏览器、辅助技术以及有无 JavaScript 时的实际行为?
  • 链接和菜单数据若来自外部输入,是否经过符合业务规则的校验与上下文正确的输出处理?
  • 需要保护的页面是否由服务端授权,而不是依赖前端隐藏菜单?

制作下拉导航的核心不只是让子菜单“出现”,还要让用户知道如何打开、如何选择,以及页面怎样响应。先确定导航层级和链接目的,再选择原生语义与布局方式;随后分别验证键盘、触屏、小屏和真实链接数据。对于简单网站导航,原生 HTML 往往足够;需要复杂状态时再引入脚本,并把可访问性和服务器端权限边界一并纳入设计。

© 版权声明

相关文章