::before无法给每行加行号,因其作用于元素整体而非换行符分割的每一行;真正可行方案是用counter-reset/counter-increment配合display:table将每行转为独立容器实现自动编号。

为什么直接用 ::before 无法给每行加行号
因为 ::before 是作用在元素整体的,不是按换行符拆分的。代码块(比如 <pre><code>)里所有文本默认是单个文本节点,::before 只能插入一次,没法“每行一个”。强行用 white-space: pre + ::before 模拟,行高、对齐、复制粘贴都会出问题。
真正可行的方案:用 counter-reset + counter-increment 配合 display: table
核心思路是把每一行变成独立的 <div> 或 <span>,再用 CSS 计数器自动编号。不需要 JS,但 HTML 结构得配合:
- 把原始代码按
\n拆成多行,每行包一层<div class="line"> - 父容器设
counter-reset: line,子元素设counter-increment: line - 用
.line::before显示计数器:content: counter(line); - 为对齐行号和代码,用
display: table布局(比flex更稳,兼容老浏览器)
示例结构:
<pre class="code-block">
<div class="line">function hello() {</div>
<div class="line"> console.log("world");</div>
<div class="line">}</div>
</pre>CSS:
立即学习“前端免费学习笔记(深入)”;
.code-block {
counter-reset: line;
display: table;
width: 100%;
}
.line {
counter-increment: line;
display: table-row;
}
.line::before {
content: counter(line);
display: table-cell;
text-align: right;
padding-right: 1em;
color: #999;
user-select: none;
}
.line > * {
display: table-cell;
}如果必须保留原始 <pre><code> 结构怎么办
那就得靠 JS 预处理——在渲染前把换行符转成 <div> 行包装。CSS 本身做不到“按 \n 切割并逐行伪元素”。常见错误是试图用 white-space: pre-line + line-height + ::before 做绝对定位模拟,结果行号漂移、缩放错位、打印失效。
- 别用
position: absolute对齐行号——字体度量变化时会偏移 - 避免给
::before设固定宽度——不同数字位数(1 vs 100)撑开不一致 - 复制代码时,
user-select: none必须加在::before上,否则行号会被一起复制
行号对齐和可访问性容易被忽略的点
行号列默认是左对齐的,但多位数(如 100)会挤占代码区域。用 text-align: right + min-width 或 width 固定列宽更稳:
.line::before {
min-width: 2.5em; /* 足够显示 999 */
text-align: right;
}另外,纯 CSS 行号对屏幕阅读器不可见,如需语义化支持,得额外加 aria-label 或用 <ol> 替代——但那样就脱离了伪元素初衷。
真要兼顾所有场景,最省心的是用 Prism.js 或 highlight.js 的行号插件,它们内部就是按行封装 + 计数器,且处理了剪贴板、键盘导航、主题适配这些细节。


















