写过表单的人都踩过这个坑——页面一打开就满屏红色错误,今天 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;
  }
}

三步下一步

  1. 找到你的表单验证 CSS:搜索项目里有没有 .touched 相关的 JS 监听器,或 input:invalid 在页面加载时就会激活的样式
  2. 替换成 :user-invalid / :user-valid:删掉 JS blur 监听器 + .touched 类,CSS 伪类自动处理交互感知逻辑
  3. 加上 @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(伪类横向对比)

评论区

0 条评论

登录后可评论。