纯CSS实现三级级联菜单:10行代码搞定效果

更新日期: 2026-04-22 阅读: 13 标签: 菜单

遇到了一个经典需求:三级级联菜单,点击父级展开子级,层层下钻。产品经理的原型里,这效果看着挺高级,实现起来却让人头大。传统的做法是:每个菜单项绑定点击事件,维护一个activeMenu状态,再通过JS控制显示隐藏。50行代码起步,状态管理还得小心翼翼。

分享一个纯CSS实现方案,只需10行代码搞定。


先看效果

不卖关子,直接看代码:

<details>
  <summary>网络设置</summary>
  <details>
    <summary>移动网络</summary>
    <details>
      <summary>数据漫游</summary>
      <p>开启后可在境外使用移动数据...</p>
    </details>
  </details>
</details>

配合一段CSS:

@scope (:has(> details[open])) to (details[open]) {
  * {
    display: none;
  }
}

就完成了层级下钻效果。展开一级,二级显示;展开二级,一级自动收起。


关键原理:Donut Scope

这个技巧的核心是CSS的@scope规则。但很多人(包括我自己一开始)对@scope的理解是错的。

误区:@scope是用来替代BEM命名规范的工具,用来限制样式作用范围。

真相:@scope真正的威力在于“条件性作用域控制”。

它的语法是这样的:

@scope (scope-root) to (scope-limit) {
  /* 这里的样式只影响scope-root和scope-limit之间的元素 */
}

形象地说,这就像是一个“甜甜圈”(donut)——中间的scope-limit不受影响,只影响外围区域。

在我们的菜单场景里:

  • Scope Root: :has(> details[open]) —— 包含一个展开details的父元素

  • Scope Limit: details[open] —— 那个被展开的details本身

  • 效果: 隐藏父details里除了当前展开项之外的所有内容

这就是“下钻”效果的魔法所在。用户点击展开子菜单时,父级菜单的其他内容被隐藏,只剩下当前展开的子菜单链。


完整代码

这是完整的可运行代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <style>
    body {
      font-family: system-ui, sans-serif;
      max-width: 400px;
      margin: 50px auto;
      padding: 20px;
    }

    details {
      border: 1px solid #ddd;
      border-radius: 8px;
      margin: 8px 0;
      overflow: hidden;
    }

    summary {
      padding: 12px 16px;
      background: #f5f5f5;
      cursor: pointer;
      list-style: none;
      display: flex;
      align-items: center;
      gap: 8px;
    }

    summary::before {
      content: '▶';
      font-size: 0.8em;
      transition: transform 0.2s;
    }

    details[open] > summary::before {
      transform: rotate(90deg);
    }

    details details {
      margin: 0;
      border: none;
      border-top: 1px solid #eee;
    }

    /* 核心:donut scope */
    @scope (:has(> details[open])) to (details[open]) {
      * {
        display: none;
      }
    }

    details details details p {
      padding: 16px;
      margin: 0;
      color: #666;
    }
  </style>
</head>
<body>
  <h2>设置</h2>

  <details>
    <summary>网络设置</summary>
    <details>
      <summary>移动网络</summary>
      <details>
        <summary>数据漫游</summary>
        <p>开启后可在境外使用移动数据。可能产生额外费用。</p>
      </details>
    </details>
    <details>
      <summary>Wi-Fi</summary>
      <p>已保存的网络:Home_5G, Office_WiFi</p>
    </details>
  </details>
</body>
</html>

你可以在Chrome 118+或Safari 17.4+里直接运行这段代码。


遇到这些坑

当我兴冲冲地把这个方案发到群里,说“终于不用写JS了”时,一位同事的回复让我瞬间冷静:

“Firefox呢?”

对啊,Firefox呢?

坑1:Firefox完全不支持

截止到2026年4月,@scope在Firefox里完全不支持。

@supports (@scope (:has(.test)) to (.test)) {
  /* 这段代码在Firefox里不会执行 */
}

这意味着如果你的用户里有Firefox用户(哪怕是5%),这个方案就用不了。不是降级 gracefully,是直接挂掉。

坑2::has()的兼容性问题

我们的代码里还用了:has()选择器。虽然:has()的支持度比@scope好,但在Safari 15.4之前的版本也不支持。

如果你的目标用户还在用旧版macOS,这又是个问题。

坑3:动画和过渡不友好

你可能注意到了,代码里只有summary::before的旋转有transition,菜单的展开收起是瞬间完成的。

这是因为display: none不支持transition。如果你想做平滑的展开收起动画,还是得用JS控制max-height或opacity。

坑4:可访问性考虑

<details>元素虽然有原生的可访问性支持,但这种“下钻”模式其实改变了默认的交互逻辑。屏幕阅读器用户可能会困惑:“为什么我展开了一个菜单,其他的内容不见了?”

如果你的产品对可访问性要求高,这个方案需要额外测试和优化。


爬坑建议

折腾了一下午,把坑都踩了一遍,我的结论是:

@scope方案不是不能用,但得看场景。

场景建议原因
内部工具/后台系统可以用浏览器可控,开发速度快
原型/概念验证可以用10行代码搞定,快
Chrome-only环境可以用Electron、Chrome Extension
面向公众的Web产品不建议Firefox不支持,兼容性风险
需要平滑动画不建议display: none不支持transition
对可访问性要求高不建议需要额外测试和优化

对比传统的JS方案:

// 传统JS方案
const menuItems = document.querySelectorAll('.menu-item');
menuItems.forEach(item => {
  item.addEventListener('click', (e) => {
    // 管理active状态
    // 控制显示隐藏
    // 处理动画
  });
});

JS方案虽然代码多,但兼容性好、可控性强。如果只是简单的三层菜单,用JS并不复杂。


总结

@scope确实很酷。这个“Donut Scope”模式展示了现代CSS的创造力——用声明式的方式解决原本需要JS的问题。

纯CSS方案能少写代码,但前提是浏览器支持。如果你的用户覆盖Firefox,或者需要复杂的交互逻辑,还是乖乖写JS吧。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13681

相关推荐

vue-contextmenujs原生右键菜单组件

vue-contextmenujs:Vue 原生实现右键菜单组件, 零依赖;npm 安装npm install vue-contextmenujs;测试中使用的是element-ui图标 ;

vue项目中的菜单权限控制

在pc 管理系统这种类型的产品,通常会涉及到账号权限的控制,不同的账号权限能浏览的功能模块是不同的,对应侧边栏菜单模块的显示也会不同。单点登录类系统,通常会多个项目公用一套登录系统,项目首页直接就是dashboard 或者 index页面

HTML下拉导航菜单的实现:CSS/Js的实现方案

熟练使用导航栏,对于网站排版非常重要,使用CSS,js,jq等你可以转换成好看的导航栏而不是枯燥的HTML菜单。

简单的树形菜单如何写?

数据结构中含有图片、名称、children的树形结构,需要展示出每一级的图片名称和图片,找了些树形图的插件,都没有展示大的图片的,一般都是小图标,就自己试着写一个包含图的简单的插件。

Flutter实现仿微信QQ侧滑菜单组件

1. 首先可以滑出菜单 2. 菜单滑动到一定距离完全滑出,未达到距离回滚 3. 菜单数量、样式随意定制 4. 菜单点击回调 5. 菜单展开时,点击 item 收回菜单,需求明了以后就可以写代码了。

前后端分离开发中动态菜单的两种实现方案

做权限管理,一个核心思想就是后端做权限控制,前端做的所有工作都只是为了提高用户体验,我们不能依靠前端展示或者隐藏一个按钮来实现权限控制,这样肯定是不安全的。就像用户注册时需要输入邮箱地址,前端校验之后,后端还是要校验

CSS多级菜单的实现

这是一个相当炫的功能,让网页看起来像桌面程序,如window的开始菜单。实现原理基本和纯CSS相册差不多,但要注意的事项比较多,让我们一步步来吧。

在Vue中实现随hash改变响应菜单高亮

Vue+Element 实现管理页面菜单栏, 点击菜单时 router 改变 hash 访问不同子组件。但是改变 hash 时菜单栏展开状态和 highlight 并不会同步, 需要手动实现。

Vue3 封装 Element Plus Menu 无限级菜单组件

本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由动态渲染菜单。

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!