多级列表嵌套合法但默认不显示“1.1”类编号,需用CSS counter实现;嵌套建议≤3层,过深应改用details或JS树形控件;li必须显式闭合,ul/ol内不可直接放div;语义决定嵌套类型,非视觉需求。

多级列表不能靠 <ul> 和 <ol> 嵌套自动产生“1.1”“2.3.1”这类编号,浏览器默认只重置子列表计数器,不继承父级序号。真要显示层级编号,必须用 CSS counter 手动控制。
嵌套本身合法但默认不显示层级编号
HTML 允许在 <li> 里直接放 <ul> 或 <ol>,结构完全合规,比如:
<ol>
<li>第一步</li>
<li>第二步</li>
<li>第三步
<ol>
<li>子步骤一</li>
<li>子步骤二</li>
</ol>
</li>
</ol>
但浏览器渲染时,内层 <ol> 会从 1 开始编号,不是 3.1、3.2 —— 这是规范行为,不是 bug。
- 嵌套层级建议 ≤3 层,再深就该考虑用
<details>或 JS 树形控件 -
<li>必须显式闭合;写成<li>a<li>b会导致解析异常(第一个<li>被隐式闭合,DOM 结构错乱) - 不要把
<div>直接塞进<ul>或<ol>,只允许<li>作为直接子元素
CSS counter 实现 1.1、1.2.1 这类编号
要用 counter-reset 和 counter-increment 搭配 counters() 函数生成嵌套编号:
.multi-level {
counter-reset: section;
}
.multi-level > li {
counter-increment: section;
}
.multi-level > li::before {
content: counters(section, ".") ". ";
}
.multi-level ol {
counter-reset: section;
margin-left: 20px;
}
关键点:
-
counters(section, ".")中的"."是分隔符,可换成"-"或"" - 子
<ol>必须重新counter-reset,否则会累加而非分层计数 -
::before插入内容不影响可访问性,但屏幕阅读器不会读出这个合成编号,所以文本里最好自带“第二步:”这类提示
Ul 和 Ol 混用要看语义,不是看缩进效果
选择嵌套类型取决于内容逻辑,而不是“想让它缩进”或“想换个符号”:
- 主项并列、子项有序 → 用
<ul><li>...<ol>...</ol></li></ul></li> <li>主项有序、子项也有序 → 用 <code><ol><li>...<ol>...</ol></li></ol></li> <li>纯为视觉缩进换符号?改用 <code>list-style-type
或::before更合适,别滥用嵌套
例如菜谱:“准备材料”是并列项(<ul>),“开始烹饪”下是顺序动作(<ol>),混用反而更准确。
用 details/summary 快速实现可折叠目录树
如果目标是交互式目录(点击展开/收起),优先用原生 <details> + <summary>:
<details>
<summary>前端开发</summary>
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
</details>
优势明显:
- 无需 JS 就支持展开/折叠,语义清晰
- 天然兼容屏幕阅读器,能正确播报“已折叠”“已展开”状态
- Chrome 12+、Firefox 49+、Edge 79+ 全支持,移动端也稳
真正难的不是怎么嵌套标签,而是判断哪些层级该由 HTML 表达、哪些该交给 CSS 控制、哪些必须用 JS 交互 —— 混淆这三者的边界,才是后期维护最头疼的地方。

















