常用网页特效代码示例:制作点击展开的折叠菜单

AI资讯12小时前发布 jdkjadmin
0 00
常用网页特效代码示例:制作点击展开的折叠菜单

常用网页特效代码示例:制作点击展开的折叠菜单

折叠菜单(也常称为手风琴菜单)把一组内容收起,用户点击标题后再查看详情。它适合收纳次级导航、常见问题、设置选项、较长的说明内容等,尤其适用于页面纵向空间有限、信息需要分组呈现的场景。

折叠并不意味着内容可以随意隐藏:重要信息不应只在用户发现某个按钮后才有机会被看到;导航项也要让人知道它们是可展开的。实现时还要同步处理键盘操作、屏幕阅读器状态和重复点击。下面先从一个不依赖框架、可直接运行的基础示例开始。

一、可直接运行的基础示例

将下面代码保存为 accordion.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>
    * {
      box-sizing: border-box;
    }

    body {
      max-width: 760px;
      margin: 40px auto;
      padding: 0 16px;
      color: #202124;
      font: 16px/1.6 system-ui, sans-serif;
    }

    .accordion-item {
      margin-bottom: 10px;
      border: 1px solid #d8dde5;
      border-radius: 8px;
      overflow: hidden;
    }

    .accordion-button {
      display: flex;
      width: 100%;
      align-items: center;
      justify-content: space-between;
      padding: 14px 16px;
      border: 0;
      background: #f6f8fa;
      color: inherit;
      font: inherit;
      text-align: left;
      cursor: pointer;
    }

    .accordion-button:hover {
      background: #edf2f7;
    }

    .accordion-button:focus-visible {
      outline: 3px solid #2878d0;
      outline-offset: -3px;
    }

    .accordion-icon {
      margin-left: 16px;
      transition: transform 180ms ease;
    }

    .accordion-button[aria-expanded="true"] .accordion-icon {
      transform: rotate(180deg);
    }

    .accordion-panel {
      padding: 0 16px 16px;
    }

    /* 确保隐藏属性不会被其他 display 样式意外覆盖 */
    .accordion-panel[hidden] {
      display: none !important;
    }

    @media (prefers-reduced-motion: reduce) {
      .accordion-icon {
        transition: none;
      }
    }
  </style>
</head>
<body>
  <h1>帮助中心</h1>

  <section class="accordion" aria-label="帮助主题">
    <div class="accordion-item">
      <h2>
        <button
          class="accordion-button"
          id="question-1"
          type="button"
          aria-expanded="false"
          aria-controls="answer-1">
          如何修改账户资料?
          <span class="accordion-icon" aria-hidden="true">⌄</span>
        </button>
      </h2>
      <div
        class="accordion-panel"
        id="answer-1"
        role="region"
        aria-labelledby="question-1"
        hidden>
        <p>登录后进入账户设置,可以修改昵称、头像和联系信息。保存后请确认页面显示了更新结果。</p>
      </div>
    </div>

    <div class="accordion-item">
      <h2>
        <button
          class="accordion-button"
          id="question-2"
          type="button"
          aria-expanded="false"
          aria-controls="answer-2">
          支持哪些浏览器?
          <span class="accordion-icon" aria-hidden="true">⌄</span>
        </button>
      </h2>
      <div
        class="accordion-panel"
        id="answer-2"
        role="region"
        aria-labelledby="question-2"
        hidden>
        <p>示例使用基础 HTML、CSS 和 JavaScript。请根据项目需要,在目标浏览器中进行实际测试。</p>
      </div>
    </div>

    <div class="accordion-item">
      <h2>
        <button
          class="accordion-button"
          id="question-3"
          type="button"
          aria-expanded="false"
          aria-controls="answer-3">
          如何联系支持团队?
          <span class="accordion-icon" aria-hidden="true">⌄</span>
        </button>
      </h2>
      <div
        class="accordion-panel"
        id="answer-3"
        role="region"
        aria-labelledby="question-3"
        hidden>
        <p>请使用产品提供的联系渠道,并在描述问题时附上必要的背景信息。</p>
      </div>
    </div>
  </section>

  <script>
    document.querySelectorAll(".accordion-button").forEach((button) => {
      button.addEventListener("click", () => {
        const panelId = button.getAttribute("aria-controls");
        const panel = document.getElementById(panelId);

        // 找不到对应内容时不继续操作,避免访问空对象。
        if (!panel) return;

        const isExpanded = button.getAttribute("aria-expanded") === "true";
        button.setAttribute("aria-expanded", String(!isExpanded));
        panel.hidden = isExpanded;
      });
    });
  </script>
</body>
</html>

二、代码中几个关键点

1. 用按钮承载点击行为

展开标题使用原生 <button>,而不是只给普通文字绑定点击事件。按钮默认支持键盘焦点,并可通过 Enter 或空格激活;type="button" 则能避免按钮放在表单中时意外提交表单。不要用只有鼠标才能操作的 div 代替按钮,除非你愿意自行补齐键盘交互、焦点管理和语义。

2. 用 ARIA 属性表达状态和关联关系

aria-controls 指向被控制内容的 ID,aria-expanded 表示当前展开状态。状态值必须随内容的显示或隐藏同步更新:"true" 表示展开,"false" 表示收起。内容区域上的 aria-labelledby 则把区域名称关联到相应按钮,role="region" 可用于把内容标记为区域。

这里的 role="region" 不是所有场景的必选项。页面中有大量条目时,为每一项都创建区域地标可能增加屏幕阅读器导航负担;可以根据内容数量和页面结构决定是否保留。无论是否使用它,按钮和内容之间的 ID 关联都要正确。

3. 用 hidden 控制内容是否显示

示例给收起的内容加上 hidden,展开时移除它。该属性不仅影响视觉呈现,通常也会让内容不参与页面布局及辅助技术的阅读。CSS 中额外声明 .accordion-panel[hidden] { display: none !important; },是为了防止项目里其他针对面板的 display 规则覆盖隐藏效果。

按钮状态和面板状态应当成对更新,不能只把内容藏起来,却让 aria-expanded 仍显示为展开;反过来,只更改 ARIA 属性也不会自动隐藏内容。

4. 为什么这里没有给面板加高度动画

示例优先采用简单、稳定的显示与隐藏方式,只让图标旋转。CSS 不能直接对 display: none 到 display: block 做传统的高度过渡;常见的 max-height 动画需要预设最大值,内容高度超过预设值时可能被裁剪,远小于预设值时动画速度也会显得不自然。

如果动画对产品体验确实重要,可以单独设计动画实现,并明确处理收起时的可访问性状态、内容高度变化和用户的减少动态效果偏好。不要为了动画而让已收起内容仍能被 Tab 键聚焦,或让屏幕阅读器继续读到本应关闭的内容。对于帮助文档和普通菜单,先保证状态正确,再决定是否增加动画。

三、改成“同一时间只展开一项”

有些 FAQ 或侧边导航希望展开一项时自动关闭其他项。此时可以在点击当前按钮前,把同一组中的其他条目全部收起。下面的脚本可替换基础示例中的脚本,适用于每个按钮都位于同一个 .accordion 容器内的结构:

<script>
  document.querySelectorAll(".accordion").forEach((accordion) => {
    accordion.addEventListener("click", (event) => {
      const button = event.target.closest(".accordion-button");

      // 忽略容器内不是折叠按钮的点击。
      if (!button || !accordion.contains(button)) return;

      const panelId = button.getAttribute("aria-controls");
      const panel = document.getElementById(panelId);
      if (!panel) return;

      const willExpand = button.getAttribute("aria-expanded") !== "true";

      accordion.querySelectorAll(".accordion-button").forEach((otherButton) => {
        const otherPanelId = otherButton.getAttribute("aria-controls");
        const otherPanel = document.getElementById(otherPanelId);
        if (!otherPanel) return;

        const isCurrent = otherButton === button;
        const expanded = isCurrent && willExpand;

        otherButton.setAttribute("aria-expanded", String(expanded));
        otherPanel.hidden = !expanded;
      });
    });
  });
</script>

这种写法使用事件委托:监听容器上的点击,再从事件目标向上查找按钮。它也适合条目由 JavaScript 后续添加的情况,但动态添加的按钮仍需符合约定的类名和 aria-controls 关系。请勿把多组菜单都放进一个监听范围不清的选择器中,否则一组菜单的点击可能误关闭另一组。

四、关键边界与常见错误

  • 按钮和面板的 ID 必须一一对应。 每个 id 在整张页面中应唯一;按钮的 aria-controls 必须与实际面板 ID 完全一致。复制条目时只改了标题,却忘记改 ID,可能导致点击错误内容。
  • 不要用链接伪装展开按钮。 如果点击只是开合内容,优先使用按钮。如果确实是导航链接,就让链接完成导航,不要把链接的默认行为和展开行为混成一个难以预期的操作。
  • 不要依赖图标表达状态。 箭头旋转只是视觉提示,不能替代按钮文字和 aria-expanded。示例把装饰图标设为 aria-hidden="true",避免屏幕阅读器将其作为额外文字朗读。
  • 不要误用表单按钮默认类型。 在 <form> 内,省略 type 的按钮通常会按提交按钮处理。展开菜单时显式设置 type="button",可以避免意外提交。
  • 注意焦点和动态插入内容。 收起面板时,正常情况下焦点仍停留在触发按钮上。若业务逻辑会从面板内部触发关闭,应避免把焦点留在已隐藏的元素中;必要时先将焦点移回对应按钮。
  • 键盘操作至少要能完成开合。 使用原生按钮即可获得常规 Tab、Enter 和空格交互。方向键、Home、End 等快捷键属于特定交互模式的增强项,并非所有折叠菜单都必须实现;如果实现,应一并设计焦点移动规则并进行键盘测试。
  • 不要把关键内容藏得太深。 搜索引擎是否以及如何处理折叠内容,取决于页面实现与抓取方式,不能把折叠等同于可靠的搜索优化手段。对价格、重要限制和关键操作提示等内容,应考虑默认展示或提供清晰的发现路径。

五、安全与参数边界

上面的实现只读取按钮上的 aria-controls 字符串,再通过 document.getElementById() 查找现有元素;示例没有把用户输入拼进 HTML,也没有调用 innerHTML。折叠菜单本身不构成安全机制,不能用它来保护私密内容、权限信息或敏感操作:页面已经交付给浏览器的内容,通常仍可能被用户查看。真正的访问控制必须由服务端在数据读取和操作接口处执行。

如果面板内容来自用户输入或外部数据,不要直接使用 innerHTML 将未经处理的字符串解析成 HTML。需要展示纯文本时,优先使用 textContent;确实要渲染富文本时,应使用经过审查的 HTML 清理方案,并结合应用的安全策略。客户端把面板设为隐藏,不会消除注入风险。

示例没有设置条目数量、ID 长度或展开内容大小等硬编码限制,因为这些限制应由具体产品需求和数据模型决定。实际项目需要关注的是:ID 不重复、引用可解析、DOM 规模合理,以及外部内容经过适当处理。不要把示例脚本中的容器类名、按钮类名当成安全校验,也不要把自定义 ARIA 属性当作数据校验规则。

六、测试清单

  1. 用鼠标点击标题,确认对应内容只切换一次,图标和展开状态同步。
  2. 使用 Tab 移动到按钮,再按 Enter 或空格,确认键盘也能展开与收起。
  3. 检查初始收起状态:面板不可见,按钮的 aria-expanded 为 "false"。
  4. 复制一个菜单条目,确认页面没有重复 ID,按钮仍控制正确的面板。
  5. 在表单附近测试按钮,确认点击展开不会触发表单提交。
  6. 用浏览器开发者工具检查控制台错误,并在需要支持的浏览器和屏幕阅读器环境中验证实际体验。

一个可靠的折叠菜单,重点不是让元素“看起来会动”,而是让视觉状态、键盘操作、辅助技术状态和实际内容保持一致。先实现语义清楚、状态同步的基础版本,再按真实需求加入单项展开、动画或动态内容处理,通常更容易维护,也更不容易引入难以发现的交互问题。

© 版权声明

相关文章