写过表单的人都踩过这个坑——页面一打开就满屏红色错误,今天 CSS 把这件事彻底原生化了
写过表单的人大概都踩过这个坑——页面一打开,邮箱格子里已经标了红色,密码强度提示已经亮起来,用户还没开始输入就被错误包围了。这是 :valid/:invalid 的问题:它们只管「是否符合规则」,不管「用户有没有动过」。2026 年的 CSS 用两个伪类把这件事彻底原生化了,连一行 JS 都不用写。
问题:旧方案为什么坑
传统做法靠 :invalid 写验证样式:
/* 页面一打开,未填写的必填项就红了 */
input:invalid {
border-color: red;
background: rgba(239, 68, 68, 0.05);
}
这段 CSS 在技术上是正确的,但在体验上是灾难。用户在填写表单时,视线里充满了红色警告,每一个空字段都是「错误」,哪怕他们只是在看表单结构,还没来得及输入任何字符。
长期以来,前端工程师的解法是引入 JS:在 input 的 blur 事件上追加一个 .touched 类,然后用 .touched:invalid 替代 :invalid:
// 10行JS,只是为了延迟验证时机
inputs.forEach(input => {
input.addEventListener(blur, () => {
input.classList.add(touched);
});
});
/* 依赖JS添加.touched类才能工作 */
input.touched:invalid {
border-color: red;
}
input.touched:valid {
border-color: green;
}
10 行 JS 换一个「等用户动过才报错」,工程上不可接受,而且当动态添加表单字段时,每个新字段都要重新绑定监听器。
方案:两个交互感知的伪类
CSS Selectors Level 4 引入了两个新伪类::user-valid 和 :user-invalid。它们的行为和 :valid/:invalid 完全一致,但多了一个约束——只有当用户与字段有过显著交互之后,才会激活。
什么叫「显著交互」?标准里的定义是:用户在字段里输入了内容,然后把焦点移走(即 blur)。光是把光标点进去再出来不算,必须实际输入过。
/* 只有用户实际输入过且留下无效值时才变红 */
input:user-invalid {
border-color: red;
background: rgba(239, 68, 68, 0.05);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* 同样,:user-valid 也在用户交互后才激活 */
input:user-valid {
border-color: green;
background: rgba(52, 211, 153, 0.05);
}
配合错误提示的显示/隐藏(同样纯 CSS):
/* 默认隐藏错误提示 */
.error-hint {
display: none;
color: red;
font-size: 0.875rem;
margin-top: 4px;
}
/* 用户交互后出现无效状态 → 显示错误提示 */
input:user-invalid ~ .error-hint {
display: block;
}
/* 用户交互后出现有效状态 → 隐藏错误提示 */
input:user-valid ~ .error-hint {
display: none;
}
对比旧方案:JS blur 监听器 + 10 行代码 + .touched 类 → 纯 CSS,3 行。
完整的感知型表单方案
CSS Selectors Level 4 提供了一整套表单感知伪类,:user-valid/:user-invalid 不是孤立的:
| 伪类 | 激活时机 | 典型场景 |
|---|---|---|
| :invalid / :valid | 页面加载后立即激活 | 输入框边框色初始状态 |
| :user-invalid / :user-valid | 用户交互后激活 | 主要验证反馈 |
| :in-range / :out-of-range | 值超出 min/max 范围时激活 | 数字滑块、日期选择 |
| :required / :optional | 根据 HTML required 属性激活 | 标记必填项标识 |
一个完整的「感知型表单」CSS 方案:
/* 基础样式 */
input {
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 10px 14px;
transition: border-color 0.2s, box-shadow 0.2s;
}
/* 用户交互后的状态——无效 */
input:user-invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* 用户交互后的状态——有效 */
input:user-valid {
border-color: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.1);
}
/* 范围类输入 */
input:in-range {
border-color: #22c55e;
}
input:out-of-range {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.05);
}
浏览器支持与渐进增强
覆盖率:全球 93%+
- Chrome 119+(2023年10月)
- Firefox 88+(2021年4月)
- Safari 16.5+(2023年5月)
- Edge 119+
2026年5月2日正式进入 Baseline Widely Available,意味着可以在生产环境直接使用,不需要 polyfill。
渐进增强写法:
/* 基线样式:所有浏览器都生效 */
input:invalid {
border-color: #f59e0b; /* 警告色而非错误色 */
}
/* 增强样式:只有支持 :user-invalid 的浏览器才会应用 */
@supports (selector(:user-invalid)) {
input:user-invalid {
border-color: #ef4444; /* 真正的错误色 */
}
/* 页面加载时去掉警告色,交互后才变红 */
input:not(:user-invalid):not(:user-valid) {
border-color: #e5e7eb;
}
}
三步下一步
- 找到你的表单验证 CSS:搜索项目里有没有
.touched相关的 JS 监听器,或input:invalid在页面加载时就会激活的样式 - 替换成 :user-invalid / :user-valid:删掉 JS blur 监听器 +
.touched类,CSS 伪类自动处理交互感知逻辑 - 加上 @supports 渐进增强:如果你的产品需要兼容 2023 年之前的浏览器,加一层
@supports降级,保留旧的 .touched 方案作为兜底
搜索来源:caniuse.com(覆盖率数据)、modern-css.com(:user-valid 实战对比)、web-platform-dx.github.io(Baseline Widely Available 2026-05-02)、MDN Web Docs(:user-valid 官方文档)、w3tweaks.com(伪类横向对比)
评论区
登录后可评论。