空 li 会破坏 ol 编号逻辑与语义结构,因其仍被计数却无实质内容,导致跳号、跨浏览器不一致及辅助技术误读;应从数据层过滤或用 CSS counters 替代原生编号。

ol 中出现空 li 会导致编号错位、语义断裂,甚至影响屏幕阅读器播报。这不是浏览器 bug,而是 HTML 规范明确要求:每个 li 应承载有效内容,空项会破坏序列逻辑。
为什么空 li 会让编号跳号或错乱
浏览器对 ol 的编号计数基于 li 元素数量,而非内容是否为空。即使 li 里只有空白字符或完全为空,它仍被计入序号计数——比如 <ol>
<li>第一步</li>
<li>
<li>第三步</li>
</ol>,渲染结果是「1. 第一步」「2. 」(空行)「3. 第三步」,但语义上第二步缺失,用户无法感知这个“空步骤”是否被跳过。
更严重的是:当 li 包含换行符或空格(如 <li>\n </li>),某些旧版 Safari 或 IE 可能将其视作“有内容”,而 Chrome/Firefox 则按规范忽略空白文本节点——造成跨浏览器编号不一致。
- 空
li不触发:empty伪类(因换行/空格不是“空”) -
start属性和reversed属性在空项后仍继续递增/递减,无法自动跳过 - 辅助技术(如 NVDA、VoiceOver)会读出“列表项 2,空白”,干扰理解
如何安全地隐藏或跳过空 li 而不破坏结构
不能靠 CSS 隐藏再指望编号自动重排——list-style-type: none 或 display: none 都不会让浏览器重新计算序号。必须从 DOM 层面移除或标记空项。
立即学习“前端免费学习笔记(深入)”;
- 服务端或构建时过滤:生成 HTML 前检查
li文本内容,用textContent.trim() === ''判定并剔除 - 前端运行时清理(仅限动态列表):
document.querySelectorAll('ol li').forEach(li => { if (!li.textContent.trim()) li.remove(); }); - 若需保留 DOM 位置(如动画占位),改用
aria-hidden="true"+tabindex="-1"并添加注释说明:<li aria-hidden="true" tabindex="-1"><!-- 空项,已跳过 --></li>
用 CSS counters 替代原生编号可绕过空项问题
原生 ol 编号不可编程跳过,但 CSS 计数器可以条件性递增。适用于需要完全控制编号逻辑的场景(如跳过禁用步骤、合并分组)。
关键点:
- 给
ol设counter-reset: step - 只对非空
li:not(:empty)执行counter-increment: step - 用
::before插入编号,避免干扰原生语义
示例:
ol.custom-counter {
counter-reset: step;
}
ol.custom-counter li:not(:empty) {
counter-increment: step;
}
ol.custom-counter li:not(:empty)::before {
content: counter(step) ". ";
font-weight: bold;
}注意::not(:empty) 在部分老浏览器中不支持,需配合 JS 回退。
真正该问的问题:这个空 li 是不是不该存在
绝大多数情况下,空 li 暴露的是数据层或模板逻辑缺陷。比如 CMS 导出时未过滤空字段、React 渲染中用了 {item && <li>{item}</li>} 但 item 是空字符串而非 null、或者后端返回了 "step": "" 却没做清洗。
比“怎么隐藏空项”更重要的是:确认业务逻辑是否允许“跳过某一步”。如果允许,应由数据源或组件逻辑决定是否渲染该 li;如果只是临时占位,用 data-placeholder 属性 + JS 控制更清晰。原生 ol 的设计前提就是“每个 li 都代表一个真实、非空的序列单元”。



















