不要仅依赖 `:valid` 或 `:placeholder-shown` 单独控制浮动标签,因为 `:valid` 依赖于 `required` 等验证属性,无约束时空值也会被视为有效;`:placeholder-shown` 在用户聚焦或输入空格时立即失效,但标签应保持上浮状态;两者均无法覆盖“空且聚焦”“非空”“空且未聚焦”全部状态。

先说几个核心判断:浮动标签这种设计模式,表面上看起来很简单,但它的 CSS 实现,尤其是仅用 :valid 或 :placeholder-shown 来操作,很容易踩坑。如果你觉得“用一个伪类就能搞定”,那大概率会在某些边界场景下出现问题。
为什么不能只靠单一伪类控制浮动标签
简而言之,:valid 这个伪类天生跟随表单验证逻辑,而不是跟随“用户是否输入了内容”。一个 input 如果没有 required、没有 type="email" 或 pattern 等校验属性,那么它空着的时候,浏览器也会认为它“有效”——:valid 会一直匹配,标签就永远无法上浮。反过来,:placeholder-shown 仅在 input 的 placeholder 真正显示时才会为 true。用户一旦点击输入框(聚焦),或者哪怕只输入了一个空格,placeholder 就会消失,该伪类立即失效。但问题是,此时标签理应保持上浮状态。所以,单独使用这两个伪类,都会遗漏某些关键状态,导致标签动画混乱。
组合使用:覆盖所有场景的正确方法
既然单独用哪个都不完美,那就把它们组合起来使用!需要覆盖的场景其实只有三种:
- 空且聚焦:用户刚点击输入框,但还没打字,标签需要上浮。
- 非空(无论是否聚焦):用户输入了内容,标签必须固定在上方。
- 空且未聚焦:页面加载后,或者用户离开输入框且没有输入内容,标签沉底。
推荐这样编写 CSS 规则:
input:placeholder-shown + label { /* 标签沉底(初始状态) */ }
input:focus + label { /* 空但已点击,标签上浮 */ }
input:valid + label { /* 有内容且通过校验,标签固定上浮 */ }
这里有一个特别容易踩坑的地方:顺序。 :placeholder-shown 的规则必须放在最后。因为 :focus 和 :valid 的优先级理论上更高,如果不把 :placeholder-shown 放在后面,它会被前面两条规则覆盖,导致初始状态的沉底效果无法生效。另外,:valid 在 input 值为 "0"(数字框)或者只输入了一个空格时,可能不会触发。如果你想更稳定地判断“非空”,:not(:placeholder-shown) 反而比 :valid 更可靠。
DOM 结构是前提,属性一个都不能少
浮动标签并不是什么纯 CSS 魔法,它对 HTML 结构有硬性要求。很多人容易在这里栽跟头:
- label 必须紧跟在 input 后面,并且必须是**下一个兄弟元素**。只有这样,才能用
+相邻兄弟选择器选中它。如果中间插入了图标、提示文字,或者把 label 放在了 input 前面,那么所有 CSS 规则都会失效。 - input 必须带有
placeholder属性,即使你写placeholder=""也可以。否则:placeholder-shown永远不会匹配,后面的组合规则也就无法生效。 - 如果你希望
:valid发挥作用,那么required或pattern等验证属性一个都不能少。没有添加required却想依靠:valid判断输入框是否有内容,基本是空想。
常见的错误还包括通过 JS 动态设置 placeholder。这样一来,:placeholder-shown 的生效时机就变得不可控,很容易出现 bug。
兼容性与边界情况:真正的挑战在这里
:valid 和 :placeholder-shown 这两个伪类的浏览器支持情况,必须心中有数:IE 完全不支持。Chrome 57+、Firefox 51+、Edge 16+、Safari 15.4+ 才可用。不过,比兼容性更麻烦的是那些边界行为。
- 用户粘贴了全角空格、零宽字符或换行符——这些东西对
:placeholder-shown来说就是“输入的内容”,它会立刻失效。但从业务逻辑来看,input 的 value 其实仍然是“空的”。这就导致了 UI 和业务判断的错位。 :valid对type="number"输入"0"的问题:数字0是一个有效值,:valid会匹配。但很多业务场景下,输入0意味着有内容,需要标签上浮。此时:valid倒是可以工作,可如果你反过来想用:invalid判断“空”,就无法实现了。- 失焦后的状态保持:如果字段非必填、又没有 pattern,那么
:valid就完全不可靠了。这时只能回退到:not(:placeholder-shown)来兜底。
从实际开发经验看,真正健壮的做法不是纠结于二选一,而是把 :not(:placeholder-shown) 当作主力,:valid 只作为一个补充的校验层。毕竟 CSS 层面无法做 trim 或正则匹配,你唯一能依赖的,就是浏览器渲染 placeholder 的那一刻。
