树形控件应以独立 tree block 递归实现,而非依赖嵌套类名;tree--nested 修饰符统一控制子树表现;tree__toggle 与 tree__children 必须紧邻;tree__item 不应用语义前缀,而用 data 属性驱动样式与行为。

树形控件的每一层都该是一个独立 tree block
不是 tree__item__subtree,也不是 tree--level-2。BEM 不靠类名深度表达嵌套,而靠 DOM 层级 + 复用 block 实现递归。你写一个 <div class="tree">,它内部某个 <code>tree__item 里再放一个 <div class="tree">,这就完成了“子树”。每个 <code>tree 都能独立控制显隐、动画、键盘导航逻辑,测试也只需针对单个 block。
常见错误是强行用元素嵌套表达层级:tree__item__children——这会让子树失去定位上下文(比如无法用 position: absolute 相对父 tree__item 定位),也破坏复用性。一旦你要把同一套子树逻辑挪到侧边栏或弹窗里,就得重写样式。
-
tree是 block,必须有完整功能闭环(展开/收起、焦点管理、可访问性属性) -
tree__item是 element,只负责单个节点的视觉与交互状态,不包含子树逻辑 - 子树实例必须直接插入
tree__item内部,不能被<span></span>、<div class="wrapper"> 或框架 Fragment 包裹,否则 JS 找节点、CSS 定位都会断链 <h3>如何用 <code>tree--nested控制缩进与动效差异二级及以下树节点需要缩进、延迟动画、右向箭头等,但这些不是“层级”本身决定的,而是“出现在子级上下文”带来的表现差异。BEM 的解法是:给子树加
tree--nested修饰符,然后在 CSS 中利用层级关系定义行为:.tree--nested { margin-left: 24px; } .tree--nested .tree__toggle::before { content: "→"; } .tree--nested .tree__children { animation-delay: 0.1s; }关键点在于:
tree--nested必须和tree共存(即class="tree tree--nested"),不能只写tree__children--nested。否则 JS 判断是否为嵌套树时,就无法通过el.classList.contains('tree--nested')统一取值。- 缩进用
margin-left而非padding-left,避免影响点击热区 -
tree--nested不改变基础结构,只叠加表现变体;一级树也能手动加这个修饰符用于特殊场景(如平铺模式) - 避免用
.tree .tree .tree这类后代选择器控制嵌套,它会耦合层级深度,且无法响应动态插入
tree__toggle和tree__children必须紧邻,否则 hover/focus 失效很多树形控件在桌面端用
.tree__toggle:hover + .tree__children控制展开,但只要中间插了一个 wrapper(比如 Vue 的<transition></transition>、React 的、甚至手写的<div class="item-content">),这个兄弟选择器就完全失效——不是 CSS 写错了,是 HTML 结构没对齐。 <p>正确结构只能是:</p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/code/12093" title="CSS婚礼策划服务机构宣传网站模板"><img src="https://img.php.cn/upload/webcode/000/000/018/178607072210981.jpg" alt="CSS婚礼策划服务机构宣传网站模板" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/code/12093" title="CSS婚礼策划服务机构宣传网站模板" class="overflowclass">CSS婚礼策划服务机构宣传网站模板</a> <p class="overflowclass">CSS婚礼策划服务机构宣传网站模板是一款适合提供婚礼策划和婚庆服务机构宣传网站模板下载。提示:本模板调用到谷歌字体库,可能会出现页面打开比较缓慢。</p> </div> <a rel="nofollow" href="/xiazai/code/12093" title="CSS婚礼策划服务机构宣传网站模板" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div> <pre class="brush:php;toolbar:false;"><button class="tree__toggle">文件夹</button> <div class="tree__children"></div></pre> <p>若框架强制插入 wrapper,必须改用 <code>.tree:hover .tree__children,但要确保.tree有position: relative,否则.tree__children的绝对定位会脱离预期容器。- 移动端必须用
:focus-within替代:hover,否则触屏设备无法触发 -
.tree__toggle必须是可聚焦元素(button或带tabindex的div),否则:focus-within不生效 - 不要依赖 JS 动态加
open类来控制显隐,那会让纯 CSS 方案退化成半吊子
为什么
tree__item不能带语义前缀,比如tree__item--file因为
tree__item本质是通用容器,它的子元素(如tree__icon、tree__label)才承载具体语义。加--file修饰符看似区分类型,实则把业务逻辑塞进了 UI 层:一旦后端返回的 type 字段从"file"改成"document",你就得全局搜替换类名,还可能漏掉 JS 里的判断。真正该做的是用 data 属性驱动样式与行为:
<div class="tree__item" data-item-type="file"> <span class="tree__icon"></span> <span class="tree__label">readme.md</span> </div>
然后用属性选择器写样式:
.tree__item[data-item-type="file"] .tree__icon::before { content: "?"; }-
tree__item--file违反 BEM 原则:修饰符应描述外观或状态(如--expanded、--disabled),而非数据类型 - data 属性可由 JS 动态设置,无需编译时硬编码;CSS 也不用随后端字段变更而改写
- 如果真需要强隔离,就把
file-icon拆成独立 block:<div class="file-icon"></div>,而不是塞进tree__item命名体系里
tree实例必须维护自己的tabindex流与aria-activedescendant状态,不能让焦点穿透到嵌套的子树内部——这要求 JS 层严格按 block 边界做 focus trap,而不是靠 CSS 类名自动推导。 - 缩进用










