网页序号总是乱糟糟,用CSS计数器咋整明白?

3,758字
16–24 分钟
in

写网页的时候,碰上那种需要自动编号的列表,比如文章目录、步骤说明、或者条款清单,手动敲序号简直能把人逼疯。今天咱们就来扒一扒CSS里的计数器(CSS Counter),这玩意儿就像个隐形的小账本,能自动给页面元素发号牌,而且花样多到能玩出花。

目录

计数器是个啥黑科技?

CSS计数器本质上就是浏览器内部维护的几个变量,专门用来给元素编号。通过counter-reset重置数字、counter-increment累加数值、再用counter()counters()函数把数值掏出来塞进伪元素里。比如做个简单的有序列表,本来<ol>自带数字样式,但想改成“第1章、第2章”这种格式,或者跳过某些序号,原生list-style就歇菜了。这时候计数器就像个听话的小助手,让指哪儿打哪儿。

举个栗子,好比食堂打饭窗口,每个人拿个号码牌。计数器就是那个发号码的机器,每来一个人,机器自动吐新号码,还能随时重置成1号重新开始。下面这段代码展示了最基础的用法:

/* 重置计数器,相当于把号码机归零 */
.counter-box {
  counter-reset: section 0;
}

/* 每次遇到.item,计数器就+1 */
.counter-box .item {
  counter-increment: section;
}

/* 把计数器的值显示出来 */
.counter-box .item::before {
  content: "第" counter(section) "项、";
  color: #e67e22;
  font-weight: bold;
}
<div class="counter-box">
  <div class="item">清洗空调滤网</div>
  <div class="item">检查燃气管道</div>
  <div class="item">测试温控器灵敏度</div>
</div>

这段HTML跑出来,每个任务前面自动加上“第1项、”“第2项、”“第3项、”,完全不用手动改数字。万一中间要插入新步骤,后面的序号自动顺延,再也不用像以前那样挨个改到手指抽筋。

计数器翻车现场?多半是这两个坑

新手玩计数器最容易掉进的坑,就是忘了counter-reset的生效范围。很多人直接把重置写在要计数的父容器上,结果嵌套多层列表的时候,内层列表的序号会继续沿用外层的计数,导致数字乱飞。比如做一个嵌套的FAQ目录,外层是章节,内层是小节,如果只重置一次,内层的序号就会从外层停下的数字继续累加,而不是从1开始。

解决方法是给每个独立计数的区域单独重置。还是打比方:食堂有多个窗口,每个窗口都有自己的号码机。红烧肉窗口从1号开始,糖醋排骨窗口也从1号开始,互不干扰。代码里就得给不同区块分别counter-reset。下面这个例子演示了如何做两级目录:

.chapter {
  counter-reset: subsection 0; /* 每个.chapter内部重置小节计数器 */
  margin-bottom: 1.5em;
}

.chapter-title {
  counter-increment: chapter; /* 章节计数器递增 */
  font-size: 1.3em;
}

.chapter-title::before {
  content: "第" counter(chapter) "章 ";
}

.subsection {
  counter-increment: subsection; /* 小节计数器递增 */
  margin-left: 1.5em;
}

.subsection::before {
  content: counter(chapter) "." counter(subsection) " ";
  background: #f0f0f0;
  padding: 0 4px;
}
<div class="chapter">
  <div class="chapter-title">环境准备</div>
  <div class="subsection">安装Node.js</div>
  <div class="subsection">配置npm镜像源</div>
</div>
<div class="chapter">
  <div class="chapter-title">代码编写</div>
  <div class="subsection">初始化项目</div>
  <div class="subsection">编写第一个组件</div>
</div>

这里每个.chapter内部都重新重置了subsection,所以第二个章节的小节编号又是从1.1、1.2开始,不会出现“2.3”这种跳级情况。要是忘了重置,第二个章节的小节就会从1.3继续累加成1.4、1.5,完全乱套。

嵌套序号玩不转?用counters()搞定

碰到深层嵌套的列表,比如“1. 第一项 > 1.1 子项 > 1.1.1 孙项”,手动维护层级简直噩梦。counter()函数只能显示当前层级的数字,而counters()函数能把所有层级的数字用分隔符串起来,就像剥洋葱一样,每一层的数字都记录在内。

还是打比方:想象一个俄罗斯套娃,每个娃娃肚子上写着自己的编号,最外面的写“1”,里面第二个写“1.1”,最里面写“1.1.1”。counters()能把这一串编号自动拼出来。实际写代码的时候,配合counter-reset在每个嵌套层级里重置下一级,然后使用counters()带两个参数:计数器名称和分隔符。

下面这套方案直接拿来做多级菜单的自动编号,小白照抄就能跑:

/* 根容器重置所有层级 */
.nest-list {
  counter-reset: level1;
  list-style: none;
  padding-left: 0;
}

.nest-list li {
  counter-increment: level1;
  position: relative;
}

/* 关键:每个li内部重置下一级计数器 */
.nest-list li {
  counter-reset: level2;
}

.nest-list li::before {
  content: counters(level1, ".") " ";
}

/* 处理第二级 */
.nest-list ul {
  counter-reset: level2;
  list-style: none;
}

.nest-list ul li {
  counter-increment: level2;
  counter-reset: level3;
}

.nest-list ul li::before {
  content: counters(level1, ".") "." counter(level2) " ";
}
<ul class="nest-list">
  <li>项目启动
    <ul>
      <li>需求调研</li>
      <li>技术选型</li>
    </ul>
  </li>
  <li>开发阶段
    <ul>
      <li>前端搭建
        <ul>
          <li>组件库配置</li>
          <li>路由设计</li>
        </ul>
      </li>
      <li>接口联调</li>
    </ul>
  </li>
</ul>

运行这段代码,第二层会显示“1.1 需求调研”、“1.2 技术选型”,第三层显示“2.1.1 组件库配置”。需要注意的是,每个<li>内部必须重置下一级计数器(比如counter-reset: level2),否则深层数字不会归零。很多新手忘了写这一行,结果第二级数字会一直累加,变成“1.3”、“1.4”而不是“1.1”、“1.2”。

老版本浏览器也能跑的备胎方案

万一项目需要兼容IE那种古董,counter()counters()照样能跑,因为CSS计数器从IE8就支持了,稳得一批。但问题在于伪元素::before在某些老旧安卓浏览器里抽风。这时候可以换个思路:不用伪元素,直接在内联元素上用content属性?不行,content只能搭配伪元素用。所以真正的备胎方案是:用JavaScript读计数器值再塞进去。但既然都上JS了,还不如直接用JS生成序号。下面这个方案纯CSS兜底,如果浏览器不支持伪元素,最多不显示序号,不会导致页面炸裂。

更稳健的做法是结合@supports做特性检测:

/* 支持伪元素的现代浏览器走计数器 */
@supports (content: counter(section)) {
  .backup-list .item::before {
    content: counter(section) ". ";
    counter-increment: section;
  }
}

/* 实在不行就手写序号,但这种情况极少 */
.no-csscounter .backup-list .item::before {
  content: "※ ";
}

然后在HTML里给根元素加个类名,通过js检测并添加.no-csscounter。不过说实话,2025年的今天,还碰到不支持::before的设备概率比中彩票还低,这段就当买个保险。

最后再扔一个常用技巧:counter()可以接受第二个参数作为样式,比如counter(section, upper-roman)能输出“Ⅰ、Ⅱ、Ⅲ”。加上list-style-type那些值,像lower-alphacjk-ideographic都能玩,中文序号“一、二、三”直接用cjk-ideographic就出来了。平时写合同条款、法规条文,用这个自动生成“第一条、第二条”简直不要太爽。