counter-reset 必须写在每个独立编号区块的直接宿主容器上,而非父级wrapper;它不穿透display:contents等截断上下文,且伪元素中设置无效,嵌套编号需每层对应counter-reset与counter-increment。

counter-reset 必须写在「计数起点容器」上,不是父级就行
很多人把 counter-reset 写在布局 wrapper(比如 .wrapper 或 .container)上,结果子元素编号乱跳、重复或从 0 开始——问题不在浏览器兼容性,而在作用域锚点错了。
它只对「后续同级及后代中满足渲染流条件」的 counter-increment 生效,且初始化动作不穿透 display: contents、contain: layout、position: absolute 等截断上下文的边界。
- 正确做法:每个独立编号区块(如每张卡片、每个章节、每个列表根节点)都显式设
counter-reset - 错误写法:
.list-wrapper { counter-reset: item; }+.list-item { counter-increment: item; },但中间有display: grid或flex容器未参与计数上下文 - Safari(iOS 16.4–17.2)特别敏感:若最外层是
display: grid,且没在该 grid 容器上设counter-reset,首个子项可能被跳过
嵌套编号时 counters() 显示空或塌缩,多半是层级断开
counters(section, ".") 要求每一层都同时满足两个条件:有 counter-increment: section,且其父级(或祖先)存在对应名称的 counter-reset。漏掉任意一层,就会静默失败或退化为单层编号。
- 常见断点:某级用了
counter-increment: subsection,但父级只写了counter-reset: section,没重置subsection -
counters()第二个参数必须是字符串字面量,不能是 CSS 变量或属性值,例如counters(step, var(--sep))无效 - 伪元素里写错位置:把
counter-increment放在::after但content在::before,会导致计数触发但无处显示
动态插入元素后编号不更新,不是 bug 是设计如此
counter-increment 是渲染时一次性触发的声明式行为,不监听 DOM 变化。JS 动态插入新元素后,即使样式规则匹配,也不会自动补计数——除非该元素被重新计算样式或强制重排。
- 临时修复:给新增元素加一个 class 触发重绘,例如
el.classList.add("force-repaint"),配合.force-repaint { animation: t 0.001s; } - 更稳方案:用
counter-reset重置整个容器,再让所有子项重新触发counter-increment(适合小批量更新) - 注意:
display: none的元素仍会触发counter-increment,而visibility: hidden不会——这点常被用来做“预占位”但实际失效
伪元素里写 counter-reset 没用,别被名字误导
counter-reset 写在 ::before 或 ::after 里,对真实 DOM 子元素完全无效。因为伪元素不构成计数器作用域的锚点,它的重置只影响自身内部的 counter() 调用(极少见场景),无法辐射到兄弟或后代元素。
- 典型误用:
h2::before { counter-reset: step; content: "Chapter "; }→ 后续.step-item仍沿用旧计数器 - 正确隔离方式:用不同计数器名,例如
.card { counter-reset: card-step; }+.card .step-item::before { counter-increment: card-step; } - 不要试图靠伪元素“接管”计数逻辑,CSS 计数器没有局部作用域机制,
counter-scope尚未标准化
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











