☰
HTML无序列表布局实战:从导航菜单到图文卡片
2026/10/15 10:45:20 网站建设 项目流程

做前端这些年,我见过不少朋友一上来就抱着 Flexbox 和 CSS Grid 啃,结果拿到一个很常见的“图文列表”布局,反而写不利索。原因不是他们不努力,而是把最基础的结构能力给低估了。今天说的标题听着很老——HTML 利用无序列表设置页面布局——但这一招至今都在大量真实页面里跑着,而且跑得很稳。

无序列表(ul)和列表项(li)本来就是为“一组同类、相互独立的条目”设计的。导航菜单、图片列表、通知列表、功能入口,这些都是天然的无序列表。把它们显示成横排菜单、卡片墙、侧边导航,CSS 只是改变了视觉表现,底层结构依然清晰。这篇文章会从结构原理讲到具体布局代码,再讲几个我踩过的坑。适合刚学前端、想真正理解布局的人,也适合写后台页面时想少走弯路的老手。

1. 为什么“不务正业”的无序列表能承担布局任务

1.1 无序列表的天然结构优势

很多人以为 ul 只是用来做“项目符号”的,比如文章里的要点列表、操作步骤。这个理解没错,但视野窄了。从 HTML 语义上看,无序列表的核心并不是那个小圆点,而是“把一组同类的条目组织在一起”。

拿导航菜单举例:一个网站顶部有“首页、产品、关于我们、联系”这几个入口,它们就是一组并列内容。这时候用 ul 包住 li,每个 li 里放一个链接,结构非常自然。就算你把小圆点全部隐藏,浏览器和辅助工具仍然能从标签名上认出这是一组列表,而不是一堆没有任何语义的 div。

这种天然的分组能力,才是无序列表能承担布局任务的根本原因。它天生适合表示“重复出现的、条目化”的内容块。图文卡片、新闻列表、商品陈列、侧边栏快捷入口,本质上都符合这个特征。

1.2 从“列表”到“盒子”的思想转换

当我讲“用无序列表布局”时,很多初学者会问:li 不是一行一行显示的吗?怎么能做成横向导航?答案很简单:li 本身是块级元素,你只需要改变它的 CSS 显示方式。

默认情况下,li 是纵向堆叠的,自带一点缩进和前面的圆点。做布局时,第一件事就是把这层“原生样式”剥掉:

  • list-style: none去掉圆点
  • margin: 0去掉外间距
  • padding: 0去掉默认缩进

剥掉之后,li 就成了一个干干净净的块级盒子。你可以在里面塞图片、标题、段落,可以用 float、Flexbox 或 Grid 控制它的排列方向。从“语义上的列表项”变成“视觉上的布局单元”,这就是整个技巧的核心思路。

很多人担心用了 ul 再做布局,语义上会不会有问题。实际上,只要内容本身确实是一组并列条目,用 ul 完全合理。比如一句话新闻列表,哪怕样式做成卡片墙,它本质上还是列表。反而是强行用 div 重造一个列表,才真的丢失了语义。

1.3 哪些场景适合用无序列表布局

我把日常开发中适合用无序列表的场景分成了三类,遇到这些情况时,可以优先考虑 ul/li:

  • 导航菜单:顶部导航、侧边导航、面包屑之后的二级菜单。
  • 内容列表:文章列表、商品列表、通知列表、评论列表。
  • 功能入口:后台管理里的操作项、设置项、图标入口。

不适合的场景也有不少。比如整个页面的大骨架,上中下三栏、左右分栏,这些属于页面级布局,应该交给更宏观的语义标签和 Grid 体系,而不是硬套一个 ul。再比如复杂的表格型数据,一行有十几个字段,那该用 table 或者专门的栅格组件,用 ul 硬撑反而别扭。

判断标准其实很朴素:如果去掉所有 CSS,单看页面文字,这些内容依然是一组“并列条目”,那用 ul 就没错。如果你只是想把若干盒子排在一起,它们之间没有“列表感”,那就老老实实用 div。

2. 先把地基打好:基础结构与样式重置

2.1 一个最小可用的 HTML 结构

写列表布局之前,我建议先固定一套干净的 HTML 模板。下面是我自己常用的“图文卡片列表”结构:

<ul class="card-list"> <li class="card-list__item"> <a class="card-list__link" href="#"> <img class="card-list__img" src="cover.jpg" alt="封面图"> <div class="card-list__body"> <h3 class="card-list__title">文章标题</h3> <p class="card-list__desc">这里是摘要内容,用来告诉读者这篇文章大概讲什么。</p> </div> </a> </li> <li class="card-list__item"> <a class="card-list__link" href="#"> <img class="card-list__img" src="cover2.jpg" alt="封面图"> <div class="card-list__body"> <h3 class="card-list__title">另一篇文章标题</h3> <p class="card-list__desc">这里是另一段摘要内容。</p> </div> </a> </li> </ul>

这个结构有几个关键点需要注意。

第一,ul 只负责表示“列表容器”,真正的内容都放在 li 里面。不要直接在 ul 下面放文字或图片,那不是合法结构。

第二,li 里面可以放任意块级内容。很多人以为 li 只能放文字,其实它可以包含 img、h3、p、div,甚至是另一个 ul。这给了我们极大的排版自由度。

第三,如果每个 li 都是一个可点击的卡片,HTML5 允许用 a 标签包裹块级内容。这样做的好处是点击区域可以覆盖整张卡片。如果你用的是老项目,担心兼容性,就把 a 放到 li 内部的标题和摘要上,区域会小一些,但结构更保守。

2.2 必须做的三件事:清除默认间距、去掉圆点、控制缩进

浏览器的默认样式里,ul 有margin-top、margin-bottom,还有padding-left。默认的 padding-left 大约是 40px,这就是为什么新写的列表总会向左缩进一块。圆点则来自list-style。

下面这段是我在绝大多数项目里都会写的基础重置:

ul, ol { margin: 0; padding: 0; list-style: none; }

如果项目只涉及少量列表,我更推荐写成“作用于 class”的方式,避免全局重置影响真正的文本列表:

.card-list { margin: 0; padding: 0; list-style: none; }

注意,去掉list-style并不会改变 ul 在 HTML 语义上的身份。在辅助技术里,它依然是一组列表。也就是说,你不会因为去掉了圆点,而丢失“这是一组条目”的信息。这一点我在实际项目里测过多次,可以放心使用。

2.3 选择器怎么写给后期维护省事

给列表布局写 CSS 时,最常见的坏习惯是直接用标签选择器,比如ul li { ... }。一旦页面里出现第二个 ul,样式就会互相打架。我建议尽量给 ul 和 li 都加上 class,或者至少给外层容器加一个唯一的 class。

一比一对比下:

/* 不推荐:影响面太大 */ ul li { padding: 10px; } /* 推荐:只影响指定的列表 */ .card-list__item { padding: 10px; }

命名方式见仁见智,但我自己习惯用“块名 + 元素名”的方式。比如card-list是块,card-list__item是列表项,card-list__title是标题。这样维护时不用翻 HTML,就能大概猜到类名的作用范围。

还有一个值得养成的习惯:给 li 设样式时尽量直接写在 li 上,而不是写ul li。哪怕你的 ul 有唯一 class,多一层标签选择器也只是增加了特指度,完全没有必要。

2.4 一个可以直接复用的基础样式

如果你只想快速生成一个整齐的纵向列表,下面这套代码可以作为起点:

.list { margin: 0; padding: 0; list-style: none; } .list__item { padding: 12px 16px; border-bottom: 1px solid #ececec; } .list__item + .list__item { margin-top: 0; }

这里用到了相邻兄弟选择器+,它的作用是让每个 li 之间的分隔线只出现一次,不会出现最后一条下面多一条线的情况。如果你想要的间距不是边框,可以改成.list__item + .list__item { margin-top: 12px; },同样能避免第一个 li 被顶出多余间距。

3. 从导航菜单到图文卡片:三种实战布局

3.1 横向导航菜单:先理解 float,再推荐 Flex

无序列表最经典的布局用途是横向导航。早期没有 Flexbox 的时候,大家会把 li 设置成float: left,让它们横着排。

HTML 结构:

<ul class="nav"> <li class="nav__item"><a class="nav__link" href="#">首页</a></li> <li class="nav__item"><a class="nav__link" href="#">产品中心</a></li> <li class="nav__item"><a class="nav__link" href="#">关于我们</a></li> <li class="nav__item"><a class="nav__link" href="#">联系我们</a></li> </ul>

老式 float 写法:

.nav { margin: 0; padding: 0; list-style: none; overflow: hidden; } .nav__item { float: left; } .nav__item + .nav__item { margin-left: 20px; } .nav__link { display: block; padding: 8px 14px; text-decoration: none; color: #333; border-radius: 4px; } .nav__link:hover { background: #f2f4f7; }

这里overflow: hidden是为了解决 float 带来的父元素高度塌陷问题。float: left之后,li 的宽高不再撑开父级 ul,所以 ul 的高度可能变成 0。加一个overflow: hidden可以触发父级的块级格式化上下文,把浮动的子元素重新“装进去”。这个技巧老前端基本都写过。

不过现在的新项目,我更推荐直接用 Flexbox:

.nav { display: flex; flex-wrap: wrap; gap: 20px; margin: 0; padding: 0; list-style: none; }

display: flex可以直接让 li 横向排列,gap负责产生间距,完全不需要浮动清理,也不需要考虑 margin 负值。这个写法在视觉上更干净,而且 li 仍然保持列表语义,只是排列方式变了。

3.2 图文卡片列表:用 Grid 让列表自动换行

图文卡片是后台系统里最常见的布局之一。比如“消息通知列表”“商品推荐列表”,每个条目都是一张小卡片,里面有图、有标题、有摘要,数量不固定。这种场景用 ul 包裹 li,再给 ul 加上 CSS Grid,效果非常理想。

延续之前的 HTML 结构,CSS 可以这样写:

.card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; margin: 0; padding: 0; list-style: none; } .card-list__item { background: #f8fafc; border-radius: 8px; overflow: hidden; } .card-list__img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } .card-list__body { padding: 16px; } .card-list__title { margin: 0 0 8px; font-size: 18px; } .card-list__desc { margin: 0; font-size: 14px; line-height: 1.6; color: #555; }

grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))的意思是:每一列的宽度最少 260px,最多占满剩余空间;如果容器够宽,就多放几列,不够就自动换行。这样做的好处是,后台加数据时不需要改 CSS,新卡片会自动排队。

图片部分我特别加了aspect-ratio: 16 / 9和object-fit: cover。前者固定了图片区域的宽高比例,后者保证图片在区域里不会变形。真实项目中,后台用户上传的图经常尺寸不统一,这两条规则几乎是刚需。

3.3 左侧图标 + 右侧文字:功能入口列表

还有一种很常见的布局:左侧一个小图标,右侧一行文字,整行可点击,鼠标放上去有背景色。后台的“设置项”“账号菜单”经常长这样。

HTML:

<ul class="feature-list"> <li class="feature-list__item"> <span class="feature-list__icon" aria-hidden="true">◎</span> <span class="feature-list__text">账户设置</span> </li> <li class="feature-list__item"> <span class="feature-list__icon" aria-hidden="true">▶</span> <span class="feature-list__text">消息通知</span> </li> <li class="feature-list__item"> <span class="feature-list__icon" aria-hidden="true">⚙</span> <span class="feature-list__text">系统偏好</span> </li> </ul>

CSS:

.feature-list { margin: 0; padding: 0; list-style: none; } .feature-list__item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease; } .feature-list__item:hover { background: #f0f2f5; } .feature-list__icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; } .feature-list__text { font-size: 14px; color: #333; }

实际项目中,图标一般会用 SVG 或图标字体替代我这里的占位字符。给图标容器设置width和height可以保证图标垂直居中,避免不同图标大小不一样导致的对不齐问题。

aria-hidden="true"加在装饰性图标上,意思是告诉读屏软件“这个图标不朗读”。如果图标是纯装饰,这个属性很有必要;如果图标本身是含义的一部分,就不要加。

4. 实际操作中的常见坑与排查办法

4.1 圆点去不掉?先检查 list-style 写在哪

有同学经常问我:明明写了list-style: none,圆点还在。大多数情况是因为选择器没通过。比如你把样式写在 ul 上,却写成.item,自然不生效。或者项目里有全局 reset,优先级更高。

排查时先用浏览器开发者工具选中 li,看它的list-style-type计算值是不是none。如果显示被覆盖,就往选择器上加一下类名,或者调整样式顺序。另外,有些第三方 UI 框架会重置列表样式,如果你的代码写在框架 reset 之前,就会被覆盖。

如果你用的是伪元素做了自定义圆点,那list-style: none当然不会生效,因为圆点根本不是原生列表标记。此时问题就变成了“为什么伪元素没清掉”,需要检查content和类名。

4.2 inline-block 列表之间的神秘空隙

早期有人为了让 li 横向排列,会写display: inline-block。这会遇到一个很经典的问题:两个 li 之间会多出 3 到 5 像素的空隙,而且这个空隙不是 margin,而是 HTML 源码里的换行符和空格造成的。

解决方案有三种:

  • 把 ul 的font-size设为 0,再给 li 单独设置字体大小。
  • 在上一个 li 的闭合标签后直接写下一个 li,中间不留换行。
  • 改用 Flexbox,彻底绕开这个模型。

我在实际项目里基本不纠结,直接上 Flexbox。因为在现代浏览器里,Flexbox 处理这类单行、多行排列都比 inline-block 顺手,也不会有空格间隙的问题。

4.3 导航链接点击区域太小怎么办

很多初次写导航的人,会把 padding 写在 li 上,而没有写在 a 上。结果鼠标只有放到文字正上方才能点击,其他区域虽然看着是导航底色,却点不动。

正确的做法是把链接变成块级元素,并把内边距放在 a 上:

.nav__link { display: block; padding: 10px 16px; }

这样整个色块都是点击区域。如果你还希望 li 本身有边框,可以把边框放在 li 上,但 padding 必须留给 a,否则边框内区域不一定可点击。这个细节在移动端尤其重要,手指的点击范围本来就比鼠标大,过小的点击区域特别容易触发误触。

4.4 float 布局带来的高度塌陷

如果你在维护老项目,用的还是 float 布局,一定要记得处理父容器高度。常见做法是给父元素加overflow: hidden,或者用 clearfix:

.clearfix::after { content: ""; display: table; clear: both; }

把这类 class 加到 ul 上,就能把浮动的 li 撑起来。不过现在新开发我几乎没有再用 float 做整体布局了,它更多的是历史包袱。遇到旧代码时,能理解原理、会修就行,不建议再用它搭新页面。

4.5 不要滥用无序列表做所有事

虽然 ul/li 很好用,但它不是万能盒子。如果你要做一个复杂的多列页面骨架,里面有侧边栏、主内容区、页脚,每个区域又是独立区块,那我不会用 ul 套这么嵌套。页面级骨架应该用清晰的语义标签加 Grid 体系。

滥用列表布局最常见的后果是 HTML 结构层级过深。比如在 li 里再套 ul,ul 里又套 li,最后整个页面嵌套六七层。维护时想找一个节点,滚动半天眼睛都花了。所以我的原则是:只要内容不是真正的“并列条目”,就不用列表。列表布局的核心价值是语义清晰,不是把所有盒子都塞进 ul。

5. 我对列表式布局的几个固执习惯

项目做多了之后,我慢慢形成了一套自己的布局习惯,在这里一并分享。

第一,我几乎不会为 ul 单独写一个全局 reset,而是给具体列表类名写样式。因为像“文章目录”“操作步骤”这类正文里的列表,我是希望保留圆点和缩进的。全局把列表样式清零,看起来省事,实际埋了不少隐患。

第二,给 li 写间距时,我更愿意用gap而不是每个 li 都设margin-bottom。Flexbox 和 Grid 的gap能把间距规则统一放在父元素上,逻辑集中,后面改动也方便。项目里如果不是需要兼容特别老的浏览器,我会先用gap,再考虑降级方案。

第三,每个列表项的内容尽量结构一致。比如 li 里要么都放标题加摘要,要么都放图标加文字,不要第一个 li 一列内容,第二个 li 突然换结构。列表布局最怕“看起来像列表,内容结构各自为政”,一旦结构不一致,CSS 就很难统一控制高度和对齐。

第四,我在写完列表布局后,会刻意用浏览器的无样式模式看一眼。把 devtools 里的样式全关掉,如果页面还能看懂“这里是几组条目”,这个列表就用得值。这个方法花不了几秒,却能帮我把结构问题提前暴露出来。

无序列表布局不是一个炫酷的技术,但它非常稳。稳到很多大型后台、政务系统里至今还在大量使用。理解它的原理后,你再去用 Flexbox 和 Grid,反而会更有底气,因为你知道自己是在“控制盒子的排列”,而不是被某个新特性牵着走。

最后再分享一个小技巧:如果你发现某个 ul 布局在不同浏览器里表现不一致,先把浏览器默认样式重置统一,再排查布局属性。大多数离谱的错位,其实都源起于你没有把默认的 padding 和 list-style 清干净。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询