:first-child匹配不到“第一个元素”的根本原因是DOM树中该元素并非父元素的第一个子节点,因换行、空格、注释等会生成#text或注释节点占据首位,导致目标元素实际索引大于0。
根本原因不是选择器写错了,而是你写的那个元素在 DOM 树里根本不是父元素的第一个子节点。浏览器解析 HTML 时,所有换行、缩进、空格、注释都会生成真实的 #text 或 节点,它们和 <p>、<li> 一样算子节点——只要它排在最前,p:first-child 就永远匹配不到后面的 <p>。
常见干扰源包括:
<ul>n<li>首页</li></ul> → 换行符生成 #text,li 是第二个子节点<ul><!-- 注释 --><li>首页</li></ul> → 注释节点抢占首位<div><h3>标题</h3><p>正文</p></div> → p:first-child 不生效,因为第一个子节点是 <h3>
打开浏览器开发者工具(F12),选中父元素,在 Elements 面板里逐个展开子节点,看第一个图标是 <p> 还是 #text 或 。别信源码缩进,只信这里显示的真实节点顺序。
如果发现第一个确实是文本或注释节点,说明 :first-child 失效是必然的——它不跳过非元素节点,也不做类型过滤,只认索引 0。
选哪个取决于你要解决的问题:
<li>」→ 用 li:first-child,但要确保结构干净(无前置节点)<li> 中 DOM 顺序最靠前的那个,不管前面有没有 <h2> 或空行」→ 用 li:first-of-type,它自动跳过非 <li> 节点li:first-of-type 在绝大多数实际场景中更可靠,尤其当 HTML 是模板生成、CMS 输出或含说明性标签时。
:first-child 的匹配完全依赖静态 DOM 结构,和 CSS 层叠、display 类型、Flex/Grid 布局都无关。但它会被以下情况悄悄绕过:
::first-child(双冒号)是伪元素写法,伪类必须用单冒号 :first-child
.nav li.active)或内联样式压掉innerHTML 替换内容,但没重置节点顺序,新插入的第一个元素未必是目标真正难调试的从来不是语法,而是你眼里的“第一个”,和浏览器眼里 DOM 树里的“第一个”,根本不是同一个东西。