隐藏导航代码怎么写:用 CSS 和 JavaScript 实现导航栏显示与收起

隐藏导航代码怎么写:用 CSS 和 JavaScript 实现导航栏显示与收起

隐藏导航代码怎么写:用 CSS 和 JavaScript 实现导航栏显示与收起

导航栏需要隐藏或收起,常见于手机端菜单、页面滚动时缩小顶部导航,以及需要腾出屏幕空间的阅读界面。实现方式看起来只是切换一个 CSS 类,实际还要考虑键盘操作、屏幕阅读器、窗口尺寸变化和无 JavaScript 时的表现。

本文先给出一份可直接运行的响应式示例,再解释不同实现方式的适用场景、参数边界和常见问题。示例中的“隐藏”只影响页面显示,不构成访问控制。

先区分几种“隐藏”需求

  • 展开/收起菜单:用户点击按钮后显示或隐藏导航链接。适合手机端菜单,也可用于较窄的侧栏。
  • 滚动时隐藏顶部栏:向下滚动时收起导航,向上滚动时重新显示。适合内容阅读页面,但不适合必须始终可访问的主导航。
  • 滚动到某个位置后隐藏:根据页面滚动距离显示或隐藏导航。适合需要在首屏展示内容、越过首屏后再收起的页面。
  • 根据页面状态隐藏:例如打开对话框时暂时隐藏背景导航。此时还需要处理焦点和遮罩,不能只把导航设为不可见。

下面的主示例实现第一种需求:宽屏时导航始终显示;窄屏时显示“菜单”按钮,点击后展开或收起导航。断点、颜色和间距都是示例值,需要按项目设计调整。

可运行示例:响应式导航展开与收起

将以下内容保存为 index.html,使用浏览器打开即可运行。示例使用原生 HTML、CSS 和 JavaScript,不依赖第三方库。代码中的脚本放在页面末尾,因此可以直接获取前面的元素。

<!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: #222;
    }

    .site-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      padding: 1rem 1.25rem;
      background: #17324d;
      color: #fff;
    }

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

    .menu-toggle {
      display: none;
      padding: 0.55rem 0.8rem;
      border: 1px solid currentColor;
      border-radius: 0.35rem;
      background: transparent;
      color: inherit;
      font: inherit;
      cursor: pointer;
    }

    .site-nav ul {
      display: flex;
      align-items: center;
      gap: 1.25rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .site-nav a {
      color: inherit;
      text-decoration: none;
    }

    .site-nav a:hover,
    .site-nav a:focus-visible {
      text-decoration: underline;
    }

    main {
      max-width: 60rem;
      margin: 2rem auto;
      padding: 0 1.25rem;
      line-height: 1.7;
    }

    @media (max-width: 48rem) {
      .menu-toggle {
        display: inline-block;
      }

      .site-header {
        flex-wrap: wrap;
      }

      .site-nav {
        flex-basis: 100%;
      }

      .site-nav ul {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.75rem;
        padding: 0.5rem 0 0.25rem;
      }

      /* JavaScript 已启用且菜单未展开时,才在窄屏隐藏导航。 */
      .js-ready .site-nav:not(.is-open) {
        display: none;
      }
    }
  </style>
</head>
<body>
  <header class="site-header">
    <a class="site-name" href="/">示例网站</a>

    <button
      class="menu-toggle"
      type="button"
      aria-controls="primary-navigation"
      aria-expanded="false"
      hidden
    >
      菜单
    </button>

    <nav
      class="site-nav"
      id="primary-navigation"
      aria-label="主导航"
    >
      <ul>
        <li><a href="#intro">介绍</a></li>
        <li><a href="#services">服务</a></li>
        <li><a href="#contact">联系</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1 id="intro">页面内容</h1>
    <p>调整浏览器宽度,或在手机尺寸下点击“菜单”按钮,观察导航的展开与收起。</p>
    <h2 id="services">服务</h2>
    <p>这里放置页面主体内容。</p>
    <h2 id="contact">联系</h2>
    <p>这里放置联系信息。</p>
  </main>

  <script>
    const toggle = document.querySelector(".menu-toggle");
    const nav = document.querySelector("#primary-navigation");
    const mobileQuery = window.matchMedia("(max-width: 48rem)");

    function setMenuOpen(open) {
      nav.classList.toggle("is-open", open);
      toggle.setAttribute("aria-expanded", String(open));
    }

    function syncMenuForViewport() {
      if (mobileQuery.matches) {
        toggle.hidden = false;
        setMenuOpen(false);
      } else {
        toggle.hidden = true;
        nav.classList.remove("is-open");
        toggle.setAttribute("aria-expanded", "false");
      }
    }

    toggle.addEventListener("click", () => {
      const isOpen = toggle.getAttribute("aria-expanded") === "true";
      setMenuOpen(!isOpen);
    });

    mobileQuery.addEventListener("change", syncMenuForViewport);

    // 先注册交互,再启用依赖 JavaScript 的窄屏隐藏样式。
    document.body.classList.add("js-ready");
    syncMenuForViewport();
  </script>
</body>
</html>

代码的工作方式

CSS 负责布局,JavaScript 负责状态

媒体查询 @media (max-width: 48rem) 决定窄屏布局:导航链接改为纵向排列,按钮显示。导航是否收起,则由 .is-open 类控制。JavaScript 不直接拼接样式字符串,而是切换类名,便于把展示规则集中在 CSS 中维护。

这里的 48rem 是响应式断点示例,不是浏览器规定的特殊值。应根据导航项长度、字体大小和布局实际效果确定断点。由于 rem 与根元素字号相关,它在多数默认设置下约为 768 CSS 像素,但用户调整默认字号时对应的像素宽度也会变化。若项目需要固定 CSS 像素断点,可以使用例如 48em 或 768px,并在目标浏览器中验证。

按钮与导航的关系要明确

按钮的 aria-controls="primary-navigation" 指向受它控制的导航区域;aria-expanded 表示当前展开状态。脚本在每次展开、收起时同步更新该属性,辅助技术才能获得一致的状态信息。导航上的 aria-label 用于说明这组链接的用途;如果页面有多个导航区域,应使用不同且有意义的名称。

type="button" 可以避免按钮位于表单中时意外触发表单提交。按钮使用原生 <button>,默认支持键盘操作;不要为了外观改成没有键盘行为的普通 <div>。

为什么脚本启用后才隐藏导航

示例中的导航默认可见,按钮默认带有 hidden。JavaScript 初始化完成后才给 body 加上 js-ready 类,窄屏 CSS 才会在菜单关闭时隐藏导航。这样即使脚本未加载或执行失败,导航链接仍可见、可用,而不是只剩一个无法工作的按钮。

脚本还监听媒体查询变化。用户旋转手机、调整窗口宽度或使用分屏时,视口可能跨过断点。进入窄屏后菜单重置为关闭;回到宽屏后按钮隐藏、导航由桌面 CSS 显示。这个处理避免了只在页面首次加载时判断宽度,导致调整窗口后状态不一致的问题。

只用 CSS 能不能隐藏导航

可以。使用媒体查询控制桌面与移动布局,或用复选框配合 :checked 展开菜单,都能实现纯 CSS 效果。但复选框方案需要处理标签点击区域、焦点样式和语义;媒体查询只能响应样式条件,无法根据用户点击、导航选择或业务状态更新 ARIA 属性。对于需要清晰交互状态的菜单,按钮加 JavaScript 通常更容易维护。

如果菜单只需要在窄屏展开,优先使用上面的渐进增强方式,而不是无条件隐藏导航。若页面要求不依赖脚本也能正常使用,应保证导航在脚本不可用时保持可见,或使用服务器渲染、组件框架的状态管理实现等价行为。

滚动时自动隐藏:适用场景与实现注意点

另一类常见需求是向下滚动时隐藏固定顶部栏,向上滚动时显示。它可能减少遮挡,但也会让用户在阅读过程中暂时找不到导航,不适合导航承担重要操作或页面切换任务的场景。若采用这种设计,应提供明确的重新显示方式,并在触屏、键盘和辅助技术操作下验证。

实现时通常比较当前滚动位置与上一次位置,再根据滚动方向切换类名。不要在每个滚动事件中执行大量布局读写;页面较复杂时,可用 requestAnimationFrame 合并更新,并避免频繁改变会触发布局的属性。滚动方向判断需要设置“抖动区间”:用户轻微滑动或触控板噪声不应让导航反复闪烁。这个区间没有通用的正确值,需要按内容密度和设备测试。

如果只需滚过首屏后隐藏,可以用固定阈值判断,例如滚动超过首屏高度后隐藏;阈值应来自实际设计,而不是随意假定所有设备首屏高度相同。窗口尺寸变化、浏览器地址栏伸缩和页面缩放都可能影响可视区域,因此应根据目标效果选择 window.innerHeight、具体元素高度或 CSS 媒体查询,并验证移动端行为。

无论采用哪种滚动方案,改变显示状态时都要保持视觉与无障碍状态一致。不要把焦点留在刚刚隐藏的链接上;如果用户正通过键盘操作导航,不宜因为一次滚动事件突然将其藏起。动画可选,但应尊重 prefers-reduced-motion,为偏好减少动态效果的用户关闭或简化过渡。

让收起动画更平滑

示例直接切换 display,状态明确、实现简单,但不会产生渐隐动画。CSS 的 display: none 不能像普通数值那样平滑过渡。若需要动画,可以改用 opacity、transform 或尺寸属性,但必须同时处理隐藏后的键盘焦点和可访问性:透明不代表不可交互,移出屏幕也不一定会让元素从辅助技术的阅读顺序中消失。

不要只把导航设为 opacity: 0,就认为它已经收起。隐藏状态还要确保链接不能被键盘误选,且辅助技术不会把它当作仍可操作的内容。可以在动画结束后设置隐藏状态,展开时先恢复可访问状态再启动动画;也可以使用经过验证的动画组件或浏览器支持的离散属性过渡方案。若不需要动画,直接使用显示/隐藏规则往往更稳妥。

常见错误与排查方法

1. 点击后视觉状态变了,屏幕阅读器状态没变

只切换 CSS 类、不更新 aria-expanded,会造成视觉和语义不一致。将状态集中在一个函数里更新,避免在多个事件处理器中分别改类名和属性。示例中的 setMenuOpen 就承担这个职责。

2. 菜单收起后仍能通过 Tab 进入链接

常见原因是只设置透明度、负向定位或变换,没有真正隐藏内容或禁用其交互。确认关闭状态下导航不显示,且其中链接不会获得焦点。使用动画时尤其要检查动画结束前后的焦点行为。

3. 桌面布局正常,窗口缩放后菜单卡在错误状态

如果脚本只在加载时读取一次屏幕宽度,没有监听视口变化,就可能出现桌面导航仍然处于移动端收起状态、或按钮仍然显示的问题。应根据媒体查询变化重新同步状态,或让组件框架在断点变化时重新计算。

4. 按钮没有名称,或使用图标后无法辨认

纯图标按钮应提供可访问名称,例如可见文本、aria-label,或屏幕阅读器可读的文本。若按钮文本会在“菜单”和“关闭”之间变化,也要确保变化与展开状态一致。不要把按钮的名称和 aria-expanded 状态写成互相矛盾的信息。

5. CSS 选择器被其他规则覆盖

检查浏览器开发者工具中的计算样式。全局规则、组件库样式或更高优先级的选择器可能覆盖导航的 display。避免到处添加 !important;先确认样式来源和加载顺序,再调整组件选择器。若使用 HTML 的 hidden 属性,也要留意项目 CSS 是否强行给元素设置了显示样式。

6. 在按钮里放链接,或用点击整个导航容器切换

按钮用于执行展开/收起操作,链接用于导航到目标地址。把链接嵌在按钮中会造成交互语义混乱;给整个导航容器绑定点击事件,也可能导致点击链接时触发展开逻辑。将事件绑定在专用按钮上,并让导航链接保持正常的链接行为。

隐藏导航不等于安全控制

CSS 隐藏、JavaScript 删除节点、折叠菜单或设置 aria-hidden,都不能保护数据或限制用户访问。页面源码、前端脚本和已经发送到浏览器的数据,通常都可通过开发者工具、网络请求或其他客户端方式查看;隐藏界面也不会阻止用户直接请求某个 URL。

如果某个页面、接口或数据需要权限,应在服务器端对每次请求进行身份验证和授权,并且只向有权限的用户返回相应数据。前端隐藏可以改善界面体验,但不能替代服务器端权限检查。也不要把密钥、令牌或不应公开的数据放在前端代码中,再试图通过隐藏导航来保护它们。

上线前检查清单

  • 窄屏和宽屏下导航都符合预期,调整窗口大小后状态不会卡住。
  • 按钮可通过键盘操作,焦点有清晰可见的样式。
  • aria-expanded 与菜单实际状态同步,aria-controls 指向正确的导航元素。
  • JavaScript 关闭或加载失败时,核心导航仍有可用的兜底方案。
  • 关闭状态下,链接不会被误操作或意外读作可用内容。
  • 如有动画,验证减少动态效果偏好;如有滚动自动隐藏,验证键盘、触屏和小屏设备。
  • 需要保护的页面和数据由服务器端做授权,而不是依赖前端隐藏。

简单导航可以从“CSS 负责布局、JavaScript 只切换状态”开始。把断点、开关状态、无脚本兜底和权限边界分别处理,比堆叠复杂动画或用一条选择器隐藏所有内容更可靠。

© 版权声明

相关文章