ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HTML ul li列表标签从入门到实战:结构、样式与语义化详解

HTML ul li列表标签从入门到实战:结构、样式与语义化详解 1. 别小看这个最基础的标签ul和li到底解决什么问题我见过不少写了三五年页面的前端真要让他在ul和li上说出个所以然反而容易卡壳。大多数人只是照着模板用或者遇到样式问题就list-style: none一把梭。这个标签看起来太简单了简单到没人愿意为它多花十分钟。但抛开ul/li是不是 HTML 里最简单的标签不谈它背后牵出来的问题一点都不简单什么时候该用列表嵌套列表怎么才不算写错为什么有人能用它拼出下拉框、导航栏、步骤条而你的列表一换浏览器就乱这些才是真正需要弄懂的东西。先说结论ul是unordered list的缩写意思是无序列表li是list item就是列表里的每一个条目。通俗点讲ul就像一个购物袋li就是袋子里一样一样的东西。它跟ol有序列表最大的区别是ul的每一条之间没有先后次序谁在前谁在后无所谓而ol的条目有明确的顺序会自带 1、2、3 这种编号。这个标签能干什么往小了说它负责描述一组并列的内容往大了说它是整个 HTML 语义化体系中非常关键的一个成员。你写的导航菜单、文章目录、商品列表、评论区楼层、图片画廊缩略图本质上都是列表。如果你理解了这一点就不会再纠结为什么别人用 ul/li 布局我非得用 divspan。因为在他们眼里这些内容的本质就是一组列表而不是一堆孤零零的盒子。1.1 先从一个能被浏览器读懂的最小例子说起新手第一步最常踩的坑不是不会写ul而是把结构写错浏览器虽然容忍了但后续样式一加就崩。标准写法是这样的ul li苹果/li li香蕉/li li橘子/li /ul这段代码在浏览器里会渲染成一个带实心圆点disc的竖排列表。ul是容器li是容器里的项目。li的数量不限一个ul里至少应该有一个li一个li可以不放任何内容但这种空列表项在实际项目中没有任何意义清理掉就好。有个细节容易被忽略ul的直接子元素必须是li。规范里允许ul直接包含的元素只有li、script、template这几个。所以像下面这种写法是不规范但浏览器会硬修的ul div我是容器/div li苹果/li /ul浏览器解析时会自动把那个div从ul里踢出来或者做隐式修正。这种修正的后果就是你以为自己在控制ul内的布局结果那个div根本不听ul的使唤样式表现完全不符合预期。后面我会专门讲这个坑。而li是什么都能装的容器它的内部可以放段落、图片、表单、标题甚至可以再放一个完整的ul。这一点非常关键也是多级列表的基础。1.2 无序、有序、自定义列表三兄弟怎么选HTML 里描述列表的标签其实有三个很多人只见过ul遇到要用编号的东西就直接手动写1. 2. 3.这完全没有必要。标签英文全称含义默认显示典型场景ulliUnordered List无序列表实心圆点导航、菜单、并列条目olliOrdered List有序列表数字编号步骤、排行、章节dldtddDescription List描述列表无符号、缩进名词解释、键值对选择的标准也很简单这一组内容如果颠倒顺序会影响理解就用ol颠倒顺序不影响理解就用ul内容是一条带标题的说明就用dl。比如操作步骤一定用ol因为你先执行第一步再执行第二步顺序不能乱而网站导航菜单用ul因为你完全可以按任意顺序展示那些链接。很多人写自定义下拉框时喜欢把选项写成一堆div后来发现键盘操作、屏幕阅读器朗读全都乱套。如果规则从一开始就定义清楚直接用ul/li很多东西会自然变得可用。2. 最基础但最容易写错的结构规则谁是谁的直接子元素聊完ul/li是干什么的接下来得把结构规则掰开揉碎。我面试前端时特别喜欢问一个题ul里能不能直接放div答案是不能。但很多候选人会犹豫因为他们没看过报错。为什么没报错因为 HTML 解析器太宽容了。浏览器在解析时碰到uldiv/div/ul会按容错处理把div移到ul外面。你打开 DevTools 看 DOM 结构会发现那段div根本不在ul里面。这种默默纠错的行为是很多诡异样式问题的根源。2.1 ul 的合法子元素以及 li 的开放性ul的子元素规范上只有三种li、script、template。后面两个属于特殊情况日常使用中你基本只需要关心li。所以最稳妥的写法永远是ul li条目一/li li条目二/li /ul但li内部没有限制标题、段落、图片、表单、音频视频都能放。你要做的只是把li当成一个普通的块级容器不要去限制它只能放文字。下面这个例子就是一个合法的图文列表项ul li img srccover.jpg alt封面 div h3文章标题/h3 p这是一段摘要。/p /div /li /ul是不是很像你平时用div写卡片的结构对li完全可以承担div承担的样式职责只是它在语义上比div多了一层我是一个列表项的含义。屏幕阅读器听到ul时会提示用户进入列表共 3 项这正是div做不到的。2.2 type、start、value被遗忘的原生气质在 HTML5 之前ul支持type属性来控制项目符号比如ul typecircle li空心圆/li /ul ul typesquare li方块/li /ul这在今天依然能工作但官方已经不建议用属性去控制样式新版规范更推荐用 CSS 的list-style-type。原因是想把结构和表现彻底分开。你只要记住一条原则结构用 HTML样式用 CSS就不会跑偏。相比之下ol的几个属性值得你多看一眼ol start5 li从第五项开始/li /ol ol reversed li倒序排列显示为 3、2、1/li /olstart表示编号从第几开始reversed表示倒序。还有一些场景需要手动指定某个li的编号ol li第一项/li li value10这一项直接显示为 10/li li这一项会显示为 11/li /ol这些都是非常实用的原生态功能很多时候你不需要用 JS 去操作序号浏览器的列表机制已经帮你算好了。2.3 默认行为的真相为什么你总觉得列表有多余的缩进几乎所有浏览器的默认样式表里都有这么一条规则ul, ol { display: block; list-style-type: disc; margin-block-start: 1em; margin-block-end: 1em; padding-inline-start: 40px; }注意padding-inline-start: 40px。你看到的那个缩进其实是ul自己的左侧内边距不是li的。这也是为什么很多人写重置样式时会把padding: 0加到ul上。如果不加在微信内置浏览器、不同版本的 Safari 上列表左右的位置可能差好几像素因为不同浏览器的默认padding不完全一样。另外ul默认的list-style-type是disc实心圆点嵌套一层就变成circle空心圆再嵌套一层又变成square方形。这是浏览器的默认行为不是 CSS 生效的结果。如果你不想让自己的嵌套列表出现圆点-空心圆-方块这种随机组合一定要显式重置。3. 多级嵌套的真正价值从导航菜单到文章目录前面提到li内部能再放一个ul这就是多级列表的由来。多级列表最典型的应用就是两级导航菜单和文章目录树。它们的共同点是内容天然具有父子层级关系用嵌套的ul/li描述最贴切。3.1 嵌套的黄金结构层层包裹兄弟项对齐一个标准的两级列表长这样ul li 前端技术 ul liHTML/li liCSS/li liJavaScript/li /ul /li li 后端技术 ul liNode.js/li liPython/li /ul /li /ul注意这个结构的关键点子级ul是放在父级li内部的而不是放在父级ul下面与li平级。很多人会写成ul li前端技术/li ul liHTML/li /ul /ul这种写法是错的因为ul的直接子元素必须且只能是li把ul摆在li旁边等于让浏览器做隐式修正最终 DOM 结构会变得莫名其妙。正确理解是li是组子级列表属于这一组的展开内容所以必须在li里面。3.2 用嵌套列表做文章目录等于白拿一个大纲文章目录非常适合用嵌套列表来写。你不需要额外写 JS 去生成目录结构只要把标题层级映射成ul/li层级就行nav aria-label文章目录 ul li第一章 引言/li li第二章 基础 ul li2.1 环境配置/li li2.2 第一个示例 ul li2.2.1 代码解析/li /ul /li /ul /li /ul /nav三层嵌套的列表视觉上有天然的层级缩进屏幕阅读器也能识别出这是一个包含子文档的大纲。配合 CSS 自定义标记可以做成非常好看的侧边栏目录。3.3 嵌套层级深了缩进和可访问性怎么处理层级一旦超过三层问题就来了浏览器的默认缩进是 40px 一层嵌套三层就是 120px在移动端很容易把内容挤没。这时候需要自己控制缩进常见做法是ul { margin: 0; padding: 0; list-style: none; } li ul { padding-left: 20px; }这行li ul只选中作为li子元素的ul然后给每个子级列表一个比父级更小的缩进。这里要特别留个心眼list-style: none会抹掉所有列表符号如果你只想去掉最外层的圆点、保留内层的符号就不能这样一刀切得写成只作用于顶层ul。这块在下一节展开讲。4. 让列表变好看去样式化、自定义符号和横向导航ul/li原生的样子非常朴素——黑点点加缩进。真正让它变得像导航栏、下拉菜单、卡片列表的是 CSS。这一节的内容能回答一个高频问题为什么我加了list-style: none还是有圆点/还是没有圆点4.1 reset 的正确姿势别把列表符号一刀切很多人写重置样式喜欢全局写ul { list-style: none; }这在大多数项目里没问题但有一个副作用当你真的需要展示一个带符号的列表时得再单独写回来。更科学的方式是按需重置ul { margin: 0; padding: 0; } ul { list-style-position: inside; /* 符号放在内容内 */ }如果你纯粹想让导航里的ul没有符号就给导航那个容器一个类名然后只在类名下清除.nav-list { list-style: none; margin: 0; padding: 0; }记住一个判断方法list-style属性在ul上是继承的但子列表的符号类型却被浏览器的默认样式覆盖了一部分。所以你看到父级清了子级还显示方块往往是因为ul li ul这条默认规则有更高的优先级需要显式覆盖ul ul { list-style: none; }。4.2 用 ::marker 精确控制每行的小圆点CSS 里有一个专门给列表项用的伪元素叫::marker它控制的是li前面的那个符号也就是::marker的盒子。以前我们想改符号颜色得用color一把梭结果文字颜色也跟着变。有了::marker之后可以单独控制li::marker { color: #e8590c; font-size: 1.2em; }如果不想用默认的 disc、circle、squarelist-style-type还能支持字符串li::marker { content: →; }但注意::marker里能改的样式有限主要就是字体、颜色、字号、文本对齐这些。想画更复杂的图标一般用::before配合background或者字符来实现.list-with-icon li { list-style: none; position: relative; padding-left: 24px; } .list-with-icon li::before { content: ✓; position: absolute; left: 0; color: #2b8a3e; }这样打勾清单就出来了而且不会因为list-style: none而影响布局。4.3 横向导航改变 display 是关键一步ul/li默认是竖排的块级元素想转成横向导航最直观的组合是用flex.nav-list { display: flex; list-style: none; margin: 0; padding: 0; } .nav-list li li { margin-left: 16px; }这里li li是相邻兄弟选择器它给除第一项之外的所有li加上左边距。用gap也可以但gap对非常老的浏览器兼容性差一些如果你的项目要兼容老环境li li更稳。还有一些老项目用display: inline-block让li横向排列这种方案最大的坑是li的换行符会在元素之间产生一个几像素的空隙导致导航项之间有奇怪的间距。这个问题背后是inline-block 把标签间的空白文本当作空格渲染导致的。用flex是最省心的其次是把父级font-size: 0再给li单独设字体大小但说实话都太绕了能用flex就用flex。4.4 一个冷知识不只 ul/li 能看起来像列表很多布局不写ul/li也能做出列表效果因为 CSS 里有一个display: list-item。把这个值赋给任意元素这个元素就会像li一样在左侧显示列表符号.item { display: list-item; list-style-type: disc; margin-left: 20px; }这个特性的问题是它不具备ul/li的语义屏幕阅读器不会把它读成一个列表项。所以它更适合纯粹想要一个圆点装饰又想省一个标签的旁路场景。常规情况下我还是建议你老老实实用ul/li。5. 实战场景拆解模拟下拉框、图文卡片与步骤条很多 UI 库Element、Ant Design 等里的下拉选择框并不是原生select而是用div ul li组合模拟的。热搜词里那句selenium 定位获取下拉框元素不是原生下拉框是 divulli 组合说的就是这种情况。这一节我们结合实际场景看看ul/li在不同组件里是怎么发挥作用的。5.1 非原生下拉框为什么自动化测试总是定位失败原生下拉框长这样select namecity option valuebeijing北京/option option valueshanghai上海/option /select如果是这种原生控件Selenium 里可以用Select这个辅助类非常方便from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.NAME, city)) select.select_by_visible_text(北京)但很多前端框架为了视觉效果统一不用select而是自己拼一个假下拉框div classcustom-select div classselect-trigger rolebutton请选择城市/div ul classselect-options li>from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC trigger driver.find_element(By.CSS_SELECTOR, .select-trigger) trigger.click() option WebDriverWait(driver, 5).until( EC.element_to_be_clickable( (By.XPATH, //ul[contains(class,select-options)]/li[text()北京]) ) ) option.click()如果你的下拉框选项非常多有时候列表项里的文本前后有空格精确匹配会失效。稳妥的办法是用contains(text(), ...)或者先拿到所有li再遍历options driver.find_elements(By.CSS_SELECTOR, .select-options li) for item in options: if item.text.strip() 北京: item.click() break这就是一个非常典型的教训定位方式必须先判断目标控件是原生元素还是模拟元素再选择对应策略。凡是看到ul/li组合出现在本该是下拉框的地方就要意识到它和原生select在自动化操作上完全不是一回事。5.2 图文卡片列表把 li 当成复用的容器li本身是块级容器所以放图片、标题、摘要、标签这些毫无压力。常见的写法是ul classcard-list li classcard img srcthumb.jpg alt h3标题/h3 p摘要文字/p /li li classcard img srcthumb2.jpg alt h3标题/h3 p摘要文字/p /li /ul样式上让每个li变成弹性容器.card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; list-style: none; margin: 0; padding: 0; } .card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; }这个写法特别适合商品列表、搜索结果、相册缩略图。你的第一反应可能是为什么不用 div这里换成ul/li的好处是语义更清楚——这确实是一组并列的商品而不是一堆无意义的盒子。更重要的是屏幕阅读器会明确告诉用户这里是一个包含 12 个商品的列表用户可以用上下键在项目间移动这个体验是div给不了的。5.3 用 ul/li 实现步骤条步骤条本质也是一个有序列表——每一步的顺序非常重要所以理论上是ol的活但很多组件库为了视觉统一直接拿ul改造。如果你不追求极端的一致性用ol更合理ol classsteps li classstep active填写资料/li li classstep确认订单/li li classstep支付完成/li /olCSS 里把list-style: none再用flex排成一行每个li::before里放上圆形序号就可以做出一个非常干净的步骤条。这种先用语义化标签再用 CSS 改变外观的做法是我最推荐的路径。5.4 用 ul/li 做标签列表、面包屑的一部分标签列表同样适用。比如你想展示一篇文章的标签ul classtag-list lia href/tag/htmlHTML/a/li lia href/tag/cssCSS/a/li lia href/tag/jsJavaScript/a/li /ul这里ul/li表示这些标签是一组并列的概念语义非常准确。配合a标签还能获得良好的鼠标点击区域和键盘可达性。面包屑导航也类似它是一串带有导航顺序的链接用ol能很好地表达当前位置的层级关系。6. 案发现场我在 ul/li 上踩过的真实问题这一节写给正在排查样式问题的你。以下问题全是我自己在项目里遇到过的有些问题藏得很深网上也很难搜到直接答案。6.1 幽灵缩进不是 margin是 padding有次我写一个新闻列表发现文字距离左侧总是有一段距离怎么调margin都没用。打开 DevTools 一看ul上有一个来自浏览器默认样式的padding-inline-start: 40px。这段内边距在 Chrome 和 Firefox 中表现得很一致但在某些国产浏览器或者旧 WebKit 内核里会变成 36px 甚至 32px导致列表和上面标题的起始位置对不齐。处理办法很简单写 reset 时顺手清掉ul, ol { margin: 0; padding: 0; }但要注意如果你用的是 Normalize.css 这类方案它默认保留padding这时候就得靠手动覆盖。6.2 点击区域太小padding 加在 a 标签还是 li 上导航菜单结构常常是ullia链接/a/li/ul。如果点击区域只有文字那么点用户手指在移动端很难点准。正确做法是把padding加在a标签上而不是li上同时让a变成块级或者弹性容器.nav-list a { display: block; padding: 10px 16px; }把 padding 加在a上好处是点击区域变大而且整个区域都是可点击的加到li上虽然视觉上差不多但实际点击li的空白区域不会触发a的跳转除非你用 JS 做了事件委托但没必要。6.3 换行产生的空隙inline-block 的历史遗留问题之前提过display: inline-block会在li之间产生空隙。这个空隙并不是 bug而是 CSS 把标签之间的换行符当成了空格来渲染。解法有三个父级font-size: 0、li不换行写在一行、或者给父级加display: flex。用flex是最干净的这也是现在主流方案。6.4 子列表的符号类型被叠加了默认情况下嵌套列表的第二层是空心圆第三层是方块。你如果在某一层的li上设置了list-style-type: none子列表的符号不会跟着消失因为list-style是可以继承的但浏览器对ul ul有一套自己的默认优先级。所以正确的是ul, li { list-style: none; }想彻底清理所有层级的符号就同时选中ul和li或者给容器加一个类名后用后代选择器全部清除。6.5 ul 里的 div 被浏览器挪位置了我见过一个线上 bug某个组件在ul下写了一个div classempty暂无数据/div用来做空状态提示。Chrome 下一切正常但 Safari 下那个空状态提示的样式怎么都对不上。右键检查元素才发现那个div被浏览器从ul里拎了出来挂在了ul的后面导致原来的 CSS 选择器根本无法命中。这种问题在 HTML 校验工具里一眼就能看出来但在运行时非常隐蔽。记住ul的直接子元素如果不是li浏览器的修正行为不会报错只会默默改变 DOM 结构。所以空状态、加载状态这些非列表项的内容放在ul外面或者放在某个li内部就是别直接放进ul。6.6 li 直接写文本在部分场景下语义丢失li里直接放裸文本是可以的比如li苹果/li。但如果li里面既有文本又有标签例如ul li价格span classprice99/span元/li /ul这种写法在屏幕上没问题但屏幕阅读器在逐条朗读时会有点混乱。更稳妥的方式是把整个内容包在一个块级元素里比如div或者p。这不是强制要求但对无障碍体验有帮助。7. 为什么语义比看起来像列表更重要如果你只是想让内容前面出现一个圆点完全可以用div加 CSS 实现。那为什么规范要专门提供ul/li原因是语义。7.1 语义化对 SEO 的影响搜索引擎的爬虫会对 HTML 结构进行语义分析判断页面内容的主次关系。一部分爬虫会把ul/li识别的列表内容当成结构化内容来处理尤其是导航菜单、文章目录、FAQ 列表这类结构。虽然你不能指望用了ul/li排名就飞起但这是内容组织良好的信号之一。更重要的是语义正确的代码能帮助搜索引擎更好地理解页面在讲什么这对长尾关键词和实体识别是有正向作用的。7.2 屏幕阅读器会怎么读这段代码屏幕阅读器比如 NVDA、VoiceOver遇到ul时会播报列表共 3 项然后用户可以用快捷键在列表项之间跳转。遇到div时屏幕阅读器只会念文本没有任何结构提示。对盲人和低视力用户来说知道这里是列表意味着他们可以用更高效的方式浏览内容而不需要从头到尾线性朗读。这就是语义化最大的价值它不一定让页面变好看但一定让页面更好用。7.3 判断标准内容是不是天然具有列表语义很多初学者不知道该不该用ul/li这里给一个实用的判断标准你看到的内容如果从信息结构上说是一组并列的条目那就应该用列表标签不管它长什么样。导航菜单一组并列的链接用ul/li。商品卡片一组并列的商品用ul/li。标签云一组并列的标签用ul/li。那什么不该用比如一个独立段落里嵌一个图标前后没有并列关系就不需要ul/li。再比如一篇文章的正文它是一段连续文本不是你罗列的条目直接写p就好。7.4 无障碍规范里的小讲究rolelist 不能被随便移除在部分无障碍实践建议中如果你用 CSS 把ul的list-style-type: none了某些屏幕阅读器仍然会把它当列表读。但如果你在父容器上设置了rolelist却被 ARIA 规范的 bug 影响在部分旧版本 Safari VoiceOver 组合下会忽略列表语义。解决方式很简单不要主动给ul加rolelist除非你有非常具体的理由。默认的ul/li就是最标准的列表语义加点多余的 ARIA 反而可能帮倒忙。8. 最后分享一个从会用变成用对的检查清单根据我个人的经验ul/li用得好不好可以从几个维度自查结构上ul的直接子元素永远是li子列表永远嵌在li内部。语义上有并列关系就用列表标签不要因为看起来不像列表就不用。样式上先重置默认的 margin、padding、list-style再按需自定义。定位上遇到模拟下拉框优先用文本定位li别想着用原生select那套。可访问性上别乱加 ARIA保留原生列表语义比画蛇添足更好。我自己早期写页面时很不爱用ul/li总觉得div一把梭更方便。后来做无障碍改造时被用户反馈这个页面怎么读起来乱糟糟给教育了一顿才明白标签的语义对真正使用它的人来说有多重要。现在我做任何并列结构的模块第一反应都是这里应该用一个ul这已经成了一种职业习惯。这种习惯不花一分钱却能实打实地提升代码质量也让我在新项目评审时能少挨几句骂。
返回列表