做网页的人应该都遇到过这个场景:老板或者甲方说"把这个数据展示一下",然后你打开HTML,一个ul套li,list-style-type: disc默认圆点,完事。交上去对方说"这也太丑了"。你心里想:这不就是列表吗?还能做出花来?
还真能。同样一组数据,有人做成了一排灰溜溜的圆点列表,有人做成了带渐变背景、悬停动画、响应式网格排列的卡片。区别不在"设计感"这种玄学的东西,就藏在五个CSS属性里。这篇文章从基础列表到高端卡片,一步步拆,每个示例都有完整代码,看完直接复制就能用。
网页列表设计的五个层级,你在第几层
| 1 | 裸奔列表:ul+li默认样式,连list-style都没改,前端初学者水平 |
| 2 | 基础美化列表:改了list-style-type、加了间距和颜色,看起来不丑了但没灵魂 |
| 3 | 自定义标记列表:用::marker或::before替换了项目符号,列表开始有设计感 |
| 4 | 卡片/表格列表:放弃ul,用grid/flex+div重构,每条数据都是一张独立卡片,信息层次分明 |
| 5 | 交互增强列表:悬停动画、过渡效果、斑马条纹、拖拽排序,列表不再是静态展示 |
一、基础功:ul和ol不只是加圆点和数字
HTML的列表标签其实有三种:ul(无序列表)、ol(有序列表)、dl(定义列表)。大部分人对ul和ol的理解停留在"ul出圆点、ol出数字",但list-style-type这个属性有几十个值,光有序列表就能玩出很多花样。
先看无序列表的标记类型:disc(实心圆)、circle(空心圆)、square(实心方块)、none(无标记,做导航栏常用)。有序列表的取值更有意思:decimal(1,2,3)、decimal-leading-zero(01,02,03)、lower-roman(i,ii,iii)、upper-roman(I,II,III)、lower-alpha(a,b,c)、upper-alpha(A,B,C),还有日文、中文、韩文等更多语言支持。
| 属性 | 作用 | 常用值 | 使用场景 |
|---|---|---|---|
| list-style-type | 控制标记形状 | disc / decimal / none / square / lower-roman | 所有列表 |
| list-style-position | 标记位置 | outside(默认) / inside | 控制标记与文字对齐 |
| list-style-image | 自定义标记图 | url("icon.png") | 需小图标的场景 |
| list-style(简写) | 三合一简写 | list-style: square inside | 快速设置 |
list-style-position这个属性很多人会忽略。默认outside的意思是标记在li内容框的外部,文字换行时不会缩进到标记下方。改成inside后,标记会嵌入文字流,换行时会缩进对齐。做文章目录、条款列表时,inside能让视觉更整齐;做导航、标签列表时,outside更合适。
dl定义列表是三个标签里最被低估的。它由dt(定义项)和dd(定义描述)组成,天生就是键值对结构。产品参数表、FAQ问答、简历信息、API文档,用dl比用table更语义化,而且CSS控制起来更灵活。dl配合grid布局能做到两栏、三栏甚至响应式的参数展示,比table的写法轻量得多。很多网站的"商品详情"区域就是用dl做的,只不过你平时没注意标签而已。
二、进阶:::marker伪元素,不写额外标签也能改项目符号

2020年之前,想自定义列表的项目符号,基本只能靠::before伪元素,写一堆position和content。后来::marker这个伪元素终于被主流浏览器全面支持了,可以直接选中li的标记部分,改颜色、改字号、改内容,不用额外加任何HTML标签。
::marker能改的属性有限,主要是color、font-size、content、font-weight等文字相关属性。不能改background、不能改padding、不能改定位。想做成带背景色的圆形数字标记怎么办?那还是得回到::before的方案。但大部分场景下,::marker已经够用了,而且代码量少一半。
/* 有序列表:数字变红色、加粗 */ol.custom-list li::marker { color: #dc2626; font-weight: bold; font-size: 18px; }/* 无序列表:把圆点改成箭头符号 */ul.arrow-list { list-style-type: none; }ul.arrow-list li::marker { content: "→ "; color: #2563eb; font-weight: bold; }注意:content属性在::marker中的支持情况不如color和font-size稳定。Safari对::marker的content支持比Chrome晚,生产环境建议做兼容测试。
一个特别实用的场景:文章目录。目录天然就是有序列表,但默认的1,2,3太呆板。用::marker把数字改成罗马数字或者加粗着色,目录瞬间就有杂志排版的感觉了。不用多写一行HTML,三行CSS搞定。
三、CSS计数器:有序列表的终极控制力
ol的有序列表有一个天生的限制:它只能从1开始,依次+1。如果你需要"第01条、第02条"这样的编号格式,或者需要跨列表连续编号,或者需要"1.1、1.2、2.1"这样的多级嵌套编号,list-style-type就不够用了。这时候CSS计数器上场。
CSS计数器通过三个属性配合:counter-reset初始化计数器,counter-increment递增计数器,在content中通过counter()或counters()函数输出计数值。这三者完全由CSS控制,不依赖HTML的ol结构,灵活性极高。
/* 自定义计数器:"第01步、第02步..." */.steps { counter-reset: step; list-style: none; }.steps li { counter-increment: step; }.steps li::before {content: "第" counter(step, decimal-leading-zero) "步";color: #2563eb; font-weight: bold; margin-right: 8px;}/* 多级嵌套:"1.1、1.2、2.1..." */.chapter { counter-reset: section; }.chapter > li { counter-increment: section; }.chapter > li::marker { content: counter(section) ". "; }.chapter > li ol { counter-reset: subsection; }.chapter > li ol > li { counter-increment: subsection; }.chapter > li ol > li::marker { content: counter(section) "." counter(subsection) " "; }counter-reset
在父容器上声明计数器名称和初始值。默认初始值为0,counter-reset: step 5 则从5开始。可同时声明多个计数器。
counter-increment
在需要计数的元素上声明,每出现一次计数器+1。可指定递增值,counter-increment: step 2 则每次+2。
counter() / counters()
counter(name, style)输出单个计数器的值。counters(name, separator)用于嵌套计数,自动拼接所有层级。
CSS计数器的应用场景比想象中多:法律条款的"第X条第Y款"编号、合同文档的条款层级、代码文档的目录树、FAQ的序号。这些需求如果靠后端生成编号或者手动写死,维护成本很高。用CSS计数器,增删列表项时编号自动更新,零维护。

四、扔掉ul:用grid和flex重构卡片式列表
传统列表最大的问题是信息扁平化。一条数据里如果有标题、描述、标签、时间、头像、操作按钮,用li堆在一起就是一团文字,没有层次。这时候应该果断放弃ul,用div+grid或者div+flex重新构建,把每条数据做成一张独立的"信息卡片"。
卡片式列表的核心思路:每条数据是一个独立的视觉单元,有自己的背景、阴影、内边距,卡片之间有固定间距。grid布局天然适合做卡片列表,因为它的列数可以根据屏幕宽度自适应。
/* 响应式卡片列表:PC三列、平板两列、手机一列 */.card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; padding: 0; list-style: none; }.card-item { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); transition: transform 0.2s, box-shadow 0.2s; }.card-item:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.12); }.card-title { font-size: 16px; font-weight: bold; color: #1a1a2e; margin-bottom: 8px; }.card-desc { font-size: 14px; color: #666; line-height: 1.6; margin-bottom: 12px; }.card-tag { display: inline-block; background: #eff6ff; color: #2563eb; font-size: 12px; padding: 2px 10px; border-radius: 12px; }一个经常被问的问题:卡片列表还用不用ul标签?语义上,如果它本质上是一组同类型数据的排列,用ul>li然后内部嵌套div做卡片样式,对屏幕阅读器更友好。但纯视觉上,div套div完全没问题。选哪个取决于你的SEO和无障碍需求。
卡片列表适合什么场景?产品列表、文章列表、团队成员介绍、项目展示、房源列表——每条数据包含3个以上信息字段的,用卡片比纯文字列表清晰得多。如果每条数据只有一行文字(比如标签列表、分类列表),就别用卡片了,反而浪费空间。
五、表格列表和斑马条纹:数据密集型列表的视觉策略
当数据量大到一定程度——几十条、上百条记录,每条有5个以上字段——卡片就不好使了。这时候表格是唯一合理的选择。但默认的table标签丑得令人发指:边框线密密麻麻像铁丝网,行高挤在一起看不清,表头和内容混在一起没有区分。
表格美化的核心策略只有四个字:减少视觉噪音。具体做法:去掉垂直边框线、保留水平浅色分隔线、斑马条纹隔行变色、表头用深色背景与内容区形成强烈对比、数字列右对齐、文字列左对齐。
| 技巧 | CSS写法 | 效果 |
|---|---|---|
| 斑马条纹 | tr:nth-child(even) { background: #f8fafc; } | 偶数行浅灰底色,视觉上隔开行 |
| 悬停高亮 | tr:hover { background: #eff6ff; } | 鼠标划过整行变蓝,像Excel选中效果 |
| 固定表头 | thead { position: sticky; top: 0; } | 滚动表格时表头始终可见 |
| 圆角表头 | th:first-child { border-radius: 8px 0 0 0; } | 表头左上和右上做圆角 |
/* 现代感数据表格:无竖线 + 斑马纹 + 悬停高亮 + 固定表头 */.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }.data-table thead { position: sticky; top: 0; z-index: 10; }.data-table th { background: #1e3a5f; color: #fff; padding: 12px 16px; text-align: left; font-weight: 500; }.data-table th:first-child { border-radius: 8px 0 0 0; }.data-table th:last-child { border-radius: 0 8px 0 0; }.data-table td { padding: 10px 16px; border-bottom: 1px solid #f0f0f0; }.data-table tr:nth-child(even) td { background: #f8fafc; }.data-table tbody tr:hover td { background: #eff6ff; }表格的移动端适配是个坑:超过4列的表格在手机上几乎没法看。两种解决方案:一是给table外面包一个div设置overflow-x:auto,让用户横向滚动;二是用CSS把表格在移动端重排成卡片形式。方案一实现简单但体验一般,方案二需要写媒体查询但体验更好。
六、时间线列表:用伪元素画竖线比你想的简单
时间线列表是网页中一个很特别的列表形式——物流轨迹、订单状态、公司发展历程、项目进度、个人经历,都天然适合用时间线来展示。很多人觉得时间线很难做,其实核心就是一条竖线加几个圆点,用CSS伪元素完全能搞定,不需要任何图片。

时间线的实现思路:在容器上画一条竖线(用::before伪元素做绝对定位的左边框),每个列表项前面放一个圆点(用li的::before伪元素做),圆点通过绝对定位对齐到竖线上。时间内容在圆点右侧排列。
/* 纯CSS时间线列表 */.timeline { position: relative; padding-left: 30px; list-style: none; }.timeline::before { content: ""; position: absolute; left: 10px; top: 0; bottom: 0; width: 2px; background: linear-gradient(to bottom, #2563eb, #9333ea); }.timeline li { position: relative; margin-bottom: 24px; padding-left: 24px; }.timeline li::before { content: ""; position: absolute; left: -24px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 3px solid #2563eb; z-index: 1; }.timeline li:first-child::before { background: #2563eb; box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); }.timeline .time { font-size: 12px; color: #94a3b8; margin-bottom: 4px; }.timeline .content { font-size: 15px; color: #333; }物流轨迹
已完成节点实心蓝色、当前节点带脉冲动画、未完成节点灰色空心。用户一眼看出包裹到哪了。
公司历程
年份在左侧,事件描述在右侧。每隔几个节点换一种颜色区分发展阶段。适合企业官网About页。
订单状态
已付款→已发货→运输中→已签收,每个节点配图标和描述文字。已完成节点之间连线变色。
时间线列表有一个进阶玩法:给不同的节点状态设置不同的颜色和样式。比如已完成节点是实心绿色圆点、当前节点是蓝色圆点加外发光动画、未完成节点是灰色空心圆点。这种状态区分不需要JavaScript,纯靠CSS类名切换就能实现。
七、列表的"最后一公里":间距、悬停、空状态、加载态
列表的基本结构和样式搭好之后,还有几个细节决定了成品是"能用"还是"好用"。这些细节单独看都很小,但加起来就是用户体验的分水岭。
间距节奏。列表项之间的间距不是随便给个margin-bottom=10px就完了。紧凑列表(如消息列表、通知列表)间距8-12px,标准列表(如文章列表、产品列表)间距16-20px,宽松列表(如卡片网格)间距20-24px。间距的一致性比间距的具体数值更重要——如果一个列表里有的间距12px有的间距16px,视觉上会有明显的不稳定感。建议用CSS变量统一管理:在:root里定义--list-gap: 16px,所有列表的gap和margin都引用这个变量。哪天老板说要调间距,改一个数字全站生效。
悬停反馈。可交互的列表项(点击跳转、点击展开)必须有悬停效果,让用户知道"这一行是可以点的"。最简单的做法是hover改背景色加一个微小的transform位移,成本极低但效果明显。卡片列表推荐translateY(-2px)加阴影加深,表格行推荐背景色从白色变浅蓝。
空状态。列表为空时别只显示一片白。加一个居中的空状态提示——图标+文字+可选的操作按钮。比如"暂无数据"配一个空盒子图标,"还没有订单,去逛逛吧"配一个购物车图标。空状态是用户体验的照妖镜,有没有设计过空状态,能看出一个列表是不是真正做完了。
加载态。数据还没回来的时候,别让用户盯着白屏。骨架屏(skeleton)是最好的加载占位方案——几条灰色的脉冲动画条纹,暗示用户"内容马上就来"。一个简单的骨架屏只需要几个div加CSS动画,不依赖任何JS库。核心代码只有三行:一个灰色背景条、一个linear-gradient的shimmer动画、一个infinite循环。
列表设计这件事说穿了并不复杂。从最基础的list-style-type属性,到::marker自定义标记,到CSS计数器做复杂编号,再到grid卡片和表格美化,每一步都在解决同一个问题:让信息被更快、更准确地看到和理解。圆点列表和卡片列表的区别,不是美不美的问题,是用户能不能在3秒内找到他想看的那条数据的问题。下次做列表的时候,先别急着写代码,想想用户真正需要的是什么信息,然后选最合适的形式去呈现它。
