CSS计数器不自动更新行号,因其仅在渲染时读取counter-increment一次,无法响应DOM文本变化;须用JS监听输入、统计换行符并动态设置各<li>的counter-increment样式来触发重算。

为什么 counter-increment 在编辑器里不自动更新行号
因为 CSS 计数器本身是静态的:它只在元素渲染时读取一次 counter-increment 值,不会监听 DOM 文本变化或光标移动。你在 <li> 里写 counter-increment: line,它只对初始 DOM 结构生效;当用户在内容editable区域里回车、删行、粘贴多行时,<li> 节点没增没减,计数器就完全“失联”了。
所以纯 CSS 方案在这里失效——你必须用 JS 主动管理行数,并让 CSS 只负责展示。
用 content: counter() 展示编号但靠 JS 更新计数器值
CSS 的 counter() 函数可以读取任意命名计数器当前值,但它不能被 JS 直接修改。JS 能做的,是通过修改元素的 counter-increment 内联样式,触发浏览器重计算。关键不是“设值”,而是“触发递增”。
- 给每行
<li>设置唯一 ID 或 data 属性(如data-line="1"),便于定位 - 用 JS 统计当前有多少个非空行(按换行符
\n分割内容后过滤空字符串) - 遍历所有
<li>,对第i行设置style.counterIncrement = `line ${i}`—— 注意不是设为i,而是设为差值,比如上一行是line 3,这行要变成line 4,就得设line 1 - 更稳妥的做法是清空旧计数器再重置:
el.style.counterReset = "line"然后对首行设counter-increment: line 1,后续每行都设counter-increment: line 0,再用伪元素显示counter(line)
监听输入并最小化 DOM 操作的刷新策略
直接在 input 或 keydown 里重排所有行号,性能差且会干扰光标位置。真实编辑器(如 CodeMirror、Monaco)都用“延迟同步”+“局部更新”:
立即学习“前端免费学习笔记(深入)”;
- 用
requestIdleCallback或防抖(如 100ms)处理输入事件,避免卡顿 - 只在检测到换行符数量变化时才更新行号,而不是每次按键都刷
- 用
Text.split('\n').length对比上次快照,若不同再批量更新<li>的counter-increment样式 - 避免用
innerHTML重写整列行号,改用el.style.counterIncrement单独控制每个节点
兼容性与行号对齐的隐藏细节
行号容器通常用 ::before 伪元素 + position: absolute 实现,但这会导致它脱离文档流,容易和内容错位。更稳的方式是把行号塞进 <li> 的第一个子元素(如 <span class="line-num">),然后用 counter() 配合 content:
li .line-num::before {
content: counter(line);
counter-increment: line;
}
但注意:如果 .line-num 是空元素,它的 counter-increment 不会生效;必须确保该元素存在且参与渲染(哪怕 visibility: hidden 也行)。另外 Safari 对 counter() 在 inline 元素里的基线对齐偶尔有偏差,加 vertical-align: text-top 更可靠。
最常被忽略的是:行号需要响应字体大小缩放、行高变化、甚至 white-space: pre-wrap 下的换行折叠——这些都会让“行”的语义和 DOM 节点不再一一对应,此时必须放弃纯列表结构,改用基于文本分析的行映射逻辑。



















