导航菜单代码示例:制作支持下拉展开的菜单元素

AI绘画教程13小时前发布 jdkjadmin
0 00
导航菜单代码示例:制作支持下拉展开的菜单元素

导航菜单代码示例:制作支持下拉展开的菜单元素

下拉导航常用于把同一主题下的多个页面收在一个入口下面,例如“产品”下的不同功能页、“资源”下的文档与示例,或网站主导航中的分类链接。它能节省导航栏空间,但也增加了一层交互:用户必须知道入口可以展开,并且能用鼠标、键盘或触屏打开和访问其中的链接。

本文先用原生 HTML 的 <details> 和 <summary> 实现一个无需 JavaScript 的导航下拉菜单,再说明它的适用边界、常见错误和安全注意事项。示例面向普通网站导航,不把它伪装成桌面软件里的“菜单控件”。

先选对交互模型

网页导航的主要任务是带用户前往其他页面,因此下拉面板里的内容通常应该是普通链接。网站导航不等同于应用程序菜单:一般不需要给链接添加 role="menuitem",也不应仅为了“看起来像菜单”就使用一整套菜单键盘交互。错误套用 ARIA 菜单角色,反而可能让读屏软件按应用菜单的方式解释控件,导致预期与实际操作不一致。

如果下拉入口只是展开或收起一组导航链接,原生 <details> 是轻量选择:

  • <summary> 提供可操作的展开入口,浏览器支持鼠标和键盘操作,并会暴露展开状态。
  • <details> 的 open 属性反映其展开状态。没有 JavaScript 时,用户仍可打开和关闭内容。
  • 面板内容可以直接使用列表和链接等语义化 HTML。

如果展开动作还要触发复杂逻辑、需要多个面板互斥、要与路由状态同步,或需要点击页面其他位置时自动关闭,则可以使用按钮和 JavaScript 实现。此时按钮应正确维护 aria-expanded,并通过 aria-controls 关联内容区域。本文先从需求较简单的原生方案开始。

可运行示例:原生 details 导航下拉

将下面代码保存为 .html 文件即可在现代浏览器中查看。示例包含站点名称、常规导航链接和一个可展开的分类。CSS 负责布局与外观,展开和收起由浏览器原生控件完成。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>下拉导航示例</title>
  <style>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: system-ui, sans-serif;
      color: #1f2937;
    }

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

    .site-nav {
      max-width: 960px;
      min-height: 64px;
      margin: 0 auto;
      padding: 0 20px;
      display: flex;
      align-items: center;
      gap: 28px;
    }

    .brand {
      color: inherit;
      font-weight: 700;
      text-decoration: none;
      white-space: nowrap;
    }

    .nav-list {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .nav-list a,
    .nav-list summary {
      display: block;
      padding: 10px 12px;
      border-radius: 6px;
      color: inherit;
      font: inherit;
      text-decoration: none;
      cursor: pointer;
    }

    .nav-list a:hover,
    .nav-list summary:hover {
      background: #f3f4f6;
    }

    .nav-list a:focus-visible,
    .nav-list summary:focus-visible {
      outline: 3px solid #2563eb;
      outline-offset: 2px;
    }

    .nav-dropdown {
      position: relative;
    }

    .nav-dropdown summary {
      list-style: none;
    }

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

    .nav-dropdown summary::after {
      content: "▾";
      display: inline-block;
      margin-left: 8px;
      transition: transform 160ms ease;
    }

    .nav-dropdown[open] summary::after {
      transform: rotate(180deg);
    }

    .dropdown-panel {
      position: absolute;
      z-index: 10;
      top: calc(100% + 6px);
      left: 0;
      width: 220px;
      margin: 0;
      padding: 6px;
      list-style: none;
      background: #fff;
      border: 1px solid #d1d5db;
      border-radius: 8px;
      box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
    }

    .dropdown-panel a {
      padding: 10px;
    }

    @media (max-width: 600px) {
      .site-nav {
        align-items: flex-start;
        flex-direction: column;
        gap: 8px;
        padding-top: 12px;
        padding-bottom: 12px;
      }

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

      .nav-dropdown {
        width: 100%;
      }

      .dropdown-panel {
        position: static;
        width: 100%;
        margin-top: 4px;
        box-shadow: none;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .nav-dropdown summary::after {
        transition: none;
      }
    }
  </style>
</head>
<body>
  <header class="site-header">
    <nav class="site-nav" aria-label="主导航">
      <a class="brand" href="/">示例站点</a>

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

        <li>
          <details class="nav-dropdown">
            <summary>产品</summary>
            <ul class="dropdown-panel">
              <li><a href="/products/overview">产品概览</a></li>
              <li><a href="/products/features">功能介绍</a></li>
              <li><a href="/products/pricing">价格方案</a></li>
            </ul>
          </details>
        </li>

        <li><a href="/docs">文档</a></li>
        <li><a href="/contact">联系我们</a></li>
      </ul>
    </nav>
  </header>
</body>
</html>

示例的结构与样式如何工作

用列表表达导航关系

<nav> 表示导航区域,aria-label="主导航" 为该区域提供名称。页面如果还有页脚导航、目录导航等多个 <nav>,应使用不同且清楚的名称,帮助辅助技术用户区分它们。导航项放在 <ul> 与 <li> 中,层级关系由真实 HTML 结构表达,而不是只靠缩进或颜色暗示。

“关于我们”“文档”等直接跳转的项目是链接;“产品”本身在这个示例中是展开入口,因此用 <summary>,并把链接放入其下方的列表。不要把一个普通的 <div> 当作按钮使用:它默认没有合适的键盘行为和控件语义。

details 的展开状态由浏览器维护

当用户激活 <summary>,浏览器会切换所属 <details> 的展开状态。默认情况下,面板关闭;如果希望初次载入时展开,可以在元素上添加 open,例如 <details open>。该属性是布尔属性,写上就表示展开;写成 open="false" 仍然表示存在该属性,并不等于关闭。

示例没有给 <summary> 手动添加 role="button" 或 aria-expanded,因为它已经处在原生 disclosure 控件中。重复添加不必要的 ARIA 属性,会增加状态同步负担,甚至造成标注状态与实际展开状态不一致。使用原生元素时,优先依赖其内建语义。

面板定位与响应式布局

桌面样式将 .nav-dropdown 设置为相对定位,再将面板绝对定位到入口下方。面板脱离常规文档流,因此不会推开导航栏里的其他项目。z-index 让面板尽量显示在相邻内容上方,但它不能突破所有层叠上下文:祖先元素若建立了新的层叠上下文,或周围容器设置了裁切,仍可能遮住面板,需要结合实际页面结构排查。

在窄屏断点下,示例把导航改为纵向排列,并将面板恢复为普通文档流中的内容。这样面板不会因为屏幕狭窄而溢出视口,也更适合触屏浏览。断点值只是示例选择,不是标准要求;应根据实际导航长度、字体缩放和页面布局测试,而不是照搬一个固定的“手机宽度”。

适用场景与边界

适合使用下拉展开的情况

  • 主导航项目较多,需要按主题归类,但分类下的链接数量仍可快速浏览。
  • “产品”“服务”“资源”等入口下有一组相关页面,用户能从入口文字推断其内容。
  • 展开内容主要是站内导航链接,而不是一组需要执行复杂操作的命令。
  • 桌面与触屏布局都经过设计,且用户无需依赖悬停才能找到子项。

不适合或需要另行设计的情况

如果所有链接都很重要,且页面空间允许,直接展示导航通常比隐藏内容更容易发现。子项数量很多时,应考虑分类页、侧边栏或专门的导航页面;把大量链接塞进小面板,会增加查找成本。若入口同时承担“跳转到分类首页”和“展开子菜单”两种职责,不要让同一个点击动作含糊不清。可以保留一个明确的分类链接,并用相邻的独立按钮负责展开,或者在分类首页与子链接之间重新组织信息架构。

原生 <details> 不会自动实现“点击菜单外部就关闭”,也不保证多个菜单互斥。它可以通过鼠标和键盘操作,但具体外观和部分细节受浏览器影响。若产品需求明确要求点击外部关闭、同一时间只允许一个面板展开、打开后自动聚焦到某个元素,或根据路由变化自动收起,就需要额外设计与测试,不能把这些行为当作 <details> 自带功能。

键盘、触屏与可访问性检查

  • 确认入口可聚焦。使用 Tab 键应能到达“产品”,按 Enter 或空格应能切换展开状态。使用原生 <summary> 时,浏览器提供了相应键盘支持。
  • 确认子链接可访问。展开后继续按 Tab,焦点应能到达面板中的各个链接,且焦点顺序符合页面阅读顺序。
  • 不要只用颜色表示交互状态。示例提供了明显的 :focus-visible 轮廓。不要移除轮廓后不给键盘用户替代提示。
  • 不要只支持悬停。纯 CSS 的 :hover 展开在触屏上不可靠,也不一定能满足键盘操作。悬停效果可以作为补充,不能成为唯一入口。
  • 提供清楚的入口文案。“更多”可能含义不明;“产品”或“产品分类”能更准确地提示内容。若使用图标作为入口,也要确保控件有可访问名称。
  • 尊重减少动态效果设置。示例用 prefers-reduced-motion 减少箭头过渡效果。交互不应依赖动画才能表达状态。

可访问性并非只靠添加 ARIA 属性完成。实际应在常用浏览器与辅助技术组合中检查导航区域的名称、控件展开状态、链接顺序和焦点可见性。尤其要避免自行添加与原生语义冲突的角色或状态。

常见错误及修正方向

错误一:鼠标移入才显示子菜单

只用 :hover 显示面板,容易导致键盘用户无法打开,触屏用户也可能遇到不稳定行为。应提供可激活的语义控件,例如 <summary> 或真正的 <button>。如果设计同时支持悬停快捷展示,也应保证点击、键盘和触屏仍能完成同一任务。

错误二:给链接套上菜单角色

站点导航里的链接通常仍然是普通链接,不应为了视觉效果加上 role="menu"、role="menuitem"。ARIA 角色可能改变辅助技术对控件的解释,却不会自动实现方向键导航、焦点管理等行为。角色一旦采用,作者就有责任实现对应交互模型。一般网站导航应优先保留原生的 nav、列表和链接语义。

错误三:用空链接或无意义的 href

将展开入口写成 <a href="#">,常会造成点击跳到页面顶部、地址栏片段变化或需要额外阻止默认行为。展开动作不是页面跳转时,应使用适合展开的控件;真正的导航目的地则使用有效链接。不要让链接只负责改变菜单显示状态。

错误四:隐藏面板后仍让内容参与操作

如果用自定义 CSS 或 JavaScript 控制面板可见性,不能只把内容移出屏幕或设为透明,却让其中链接继续进入 Tab 顺序或被辅助技术读取。隐藏与显示应使用合适的状态管理方式,确保视觉状态、键盘可达性和辅助技术可见性一致。使用原生 <details> 可以避免自行维护一部分隐藏状态逻辑。

错误五:面板被裁切或被其他内容遮挡

即使设置了较大的 z-index,面板也可能受祖先的 overflow: hidden、层叠上下文或其他定位元素影响。排查时应检查面板的祖先容器、层叠上下文与页面布局,不要只不断增加 z-index 数值。窄屏下将面板放回文档流,往往比坚持悬浮定位更稳妥。

需要 JavaScript 时:状态要同步

当需求超出原生控件能力时,可以用按钮控制一个内容区域。下面是状态管理的核心示例,不是前面完整页面的替代代码。按钮的 aria-expanded 要与面板是否隐藏保持一致;点击按钮时,两者一起更新。

<button type="button"
        aria-expanded="false"
        aria-controls="products-panel"
        id="products-toggle">
  产品
</button>

<ul id="products-panel" hidden>
  <li><a href="/products/overview">产品概览</a></li>
  <li><a href="/products/features">功能介绍</a></li>
</ul>

<script>
  const toggle = document.querySelector("#products-toggle");
  const panel = document.querySelector("#products-panel");

  toggle.addEventListener("click", () => {
    const isExpanded = toggle.getAttribute("aria-expanded") === "true";
    toggle.setAttribute("aria-expanded", String(!isExpanded));
    panel.hidden = isExpanded;
  });
</script>

这段代码展示的是最小的开合逻辑。它没有实现点击外部关闭、按 Escape 关闭、焦点移动、多个菜单互斥或移动端导航折叠等行为。若产品要求这些功能,应逐项定义预期,再补齐实现和测试。尤其是自动移动焦点:普通导航展开后通常可以让焦点留在触发按钮上,让用户自然 Tab 到链接;不要无理由地抢夺焦点。

如果加入点击外部关闭逻辑,要避免在面板内点击链接时被误判为外部点击;如果加入 Escape 关闭,要明确关闭后焦点是否返回触发按钮。若面板随页面滚动、路由切换或导航状态变化,也要确保 ARIA 状态、隐藏状态和视觉状态不会分离。交互状态不能只更新其中一个表现层。

安全注意事项:链接地址和动态内容

固定写在 HTML 里的站内链接通常不涉及动态内容注入,但只要菜单项来自接口、用户输入、配置文件或第三方数据,就要把“显示文本”和“链接地址”分别按用途处理。

  • 动态文本应作为文本插入,而不是未经处理地拼接成 HTML。客户端代码优先使用 textContent 创建文本节点;不要把不可信字符串直接传给 innerHTML。
  • 动态链接地址需要按业务规则校验。若只允许本站链接,应限制为预期的站内路径;若允许外部 URL,应解析并检查协议和目标规则。不要因为字符串看起来像 URL 就认为它安全。
  • 避免把不可信值放进事件处理属性、脚本片段或未经验证的 URL 属性。HTML 转义只能解决特定上下文中的字符解析问题,不等于通用安全校验。
  • 新窗口打开外部链接时,应按实际需求设置 target="_blank",并使用适当的 rel 值,例如 rel="noopener noreferrer"。是否使用 noreferrer 会影响来源信息传递,应结合产品需求决定。

HTML 转义、URL 校验和内容安全策略解决的是不同问题,不能互相替代。也不要把下拉菜单当作权限控制:隐藏一个链接不会阻止用户直接访问对应 URL。页面和接口的访问权限必须由服务端按用户身份与授权规则验证。

上线前的检查清单

  • 下拉项是否确实能帮助用户理解和查找,而不是为了把重要入口藏起来?
  • 菜单入口是否明确区分“展开”与“跳转”?链接是否都有有效目的地?
  • 键盘用户能否打开面板、访问所有链接,并看清当前焦点?
  • 窄屏、放大字体和较长文案下,面板是否仍可见、可操作且不溢出?
  • 面板与相邻内容是否存在裁切、遮挡或层叠问题?
  • 如果使用 JavaScript,按钮状态、面板可见性与 ARIA 状态是否始终一致?
  • 如果菜单项是动态数据,文本和 URL 是否分别经过适合其上下文的处理?

对于多数简单的网站导航,原生 <details>、<summary>、列表和链接已经足够。先用原生语义满足核心需求,再根据真实交互要求增加脚本,比一开始就自制复杂菜单更容易维护,也更不容易遗漏键盘和辅助技术行为。

© 版权声明

相关文章