UI 伪类

在前面的文章中,我们大致介绍了各种表单控件的样式。这包括一些伪类的使用,例如,使用 :checked 来仅在复选框被选中时对其进行样式设置。在本文中,我们将探讨可用于在不同状态下为表单设置样式的不同 UI 伪类。

预备知识 HTMLCSS 的基本了解,包括对 伪类和伪元素 的一般知识。
目标 了解表单的哪些部分难以设置样式以及原因;学习如何自定义它们。

我们有哪些伪类可用?

你可能已经熟悉以下伪类

  • :hover:仅在鼠标指针悬停在其上方时选择元素。
  • :focus:仅在元素获得焦点时选择元素(即,通过键盘 Tab 键选中)。
  • :active:仅在元素被激活时选择元素(即,当它被点击时,或在键盘激活的情况下按下 Return / Enter 键时)。

CSS 选择器 提供了几个与 HTML 表单相关的其他伪类。这些伪类提供了几个可以利用的有用目标条件。我们将在下面的部分中更详细地讨论这些伪类,但简而言之,我们将主要关注以下几个:

还有许多其他的伪类,但上面列出的那些是最明显有用的。其中一些旨在解决非常特定的利基问题。上面列出的 UI 伪类具有出色的浏览器支持,但当然,您应该仔细测试您的表单实现,以确保它们适用于您的目标受众。

注意:此处讨论的一些伪类涉及根据其验证状态(数据是否有效?)为表单控件设置样式。您将在下一篇文章(客户端表单验证)中学习更多关于设置和控制验证约束的知识,但目前我们将保持表单验证的简单性,以免混淆。

根据输入是否为必需来设置样式

关于客户端表单验证的最基本概念之一是表单输入是必需(在提交表单之前必须填写)还是可选。

<input><select><textarea> 元素都有一个 required 属性,当设置此属性时,意味着您必须填写该控件,表单才能成功提交。例如,下面表单中的名字和姓氏是必需的,但电子邮件地址是可选的。

html
<form>
  <fieldset>
    <legend>Feedback form</legend>
    <div>
      <label for="fname">First name: </label>
      <input id="fname" name="fname" type="text" required />
    </div>
    <div>
      <label for="lname">Last name: </label>
      <input id="lname" name="lname" type="text" required />
    </div>
    <div>
      <label for="email"> Email address (if you want a response): </label>
      <input id="email" name="email" type="email" />
    </div>
    <div><button>Submit</button></div>
  </fieldset>
</form>

您可以使用 :required:optional 伪类来匹配这两种状态。例如,如果我们将以下 CSS 应用到上面的 HTML 上:

css
input:required {
  border: 2px solid;
}

input:optional {
  border: 2px dashed;
}

必需的控件有实线边框,可选的控件有虚线边框。您也可以尝试不填写就提交表单,查看浏览器默认给出的客户端验证错误消息。

一般来说,您应该避免在表单中仅使用颜色来区分“必需”和“可选”元素,因为这对色盲人士不友好。

css
input:required {
  border: 2px solid red;
}

input:optional {
  border: 2px solid green;
}

网络上对于“必需”状态的标准惯例是星号(*),或与相应控件关联的单词“必需”。在下一节中,我们将看一个更好的示例,说明如何使用 :required 和生成内容来指示必需字段。

注意:您可能不会经常使用 :optional 伪类。表单控件默认是可选的,因此您可以默认设置可选样式,然后为必需控件添加额外的样式。

注意:如果同一名称组中的一个单选按钮设置了 required 属性,那么在选中其中一个之前,所有单选按钮都将是无效的,但只有设置了该属性的单选按钮才会实际匹配 :required

将生成内容与伪类一起使用

在之前的文章中,我们看到了生成内容的用法,但我们认为现在是时候更详细地讨论它了。

其思想是我们可以使用 ::before::after 伪元素以及 content 属性,使一块内容出现在受影响元素之前或之后。这块内容不会添加到 DOM 中,因此可能对某些屏幕阅读器不可见。因为它是一个伪元素,所以可以像任何实际的 DOM 节点一样对其进行样式设置。

当您想为元素添加视觉指示器(例如标签或图标)时,这非常有用,同时还提供替代指示器以确保所有用户的可访问性。例如,当选中单选按钮时,我们可以使用生成内容来处理自定义单选按钮内部圆圈的放置和动画。

css
input[type="radio"]::before {
  display: block;
  content: " ";
  width: 10px;
  height: 10px;
  border-radius: 6px;
  background-color: red;
  font-size: 1.2em;
  transform: translate(3px, 3px) scale(0);
  transform-origin: center;
  transition: all 0.3s ease-in;
}

input[type="radio"]:checked::before {
  transform: translate(3px, 3px) scale(1);
  transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2);
}

这非常有用 — 屏幕阅读器已经让用户知道他们遇到的单选按钮或复选框是否被选中,因此您不希望它们读出另一个指示选中的 DOM 元素 — 那可能会令人困惑。拥有一个纯粹的视觉指示器可以解决这个问题。

并非所有 <input> 类型都支持在其上放置生成内容。所有显示动态文本的输入类型,如 textpasswordbutton,都不显示生成内容。其他类型,包括 rangecolorcheckbox 等,则显示生成内容。

回到我们之前的必需/可选示例,这次我们将不改变输入本身的样式 — 我们将使用生成内容来添加指示标签。

首先,我们将在表单顶部添加一个段落,说明您正在寻找什么。

html
<p>Required fields are labeled with "required".</p>

屏幕阅读器用户在到达每个必需输入时,会听到“必需”作为额外的信息,而有视力的用户则会看到我们的标签。

如前所述,文本输入不支持生成内容,因此我们添加一个空的 <span> 来承载生成内容。

html
<div>
  <label for="fname">First name: </label>
  <input id="fname" name="fname" type="text" required />
  <span></span>
</div>

直接问题是 span 会在新行中显示在输入下方,因为输入和标签都设置为 width: 100%。为了解决这个问题,我们为父 <div> 设置样式,使其成为弹性容器,但同时告诉它在内容过长时将其内容换行。

css
fieldset > div {
  margin-bottom: 20px;
  display: flex;
  flex-flow: row wrap;
}

其效果是标签和输入分行显示,因为它们都设置为 width: 100%,而 <span> 的宽度为 0,因此它可以与输入显示在同一行。

现在来看生成的内容。我们使用以下 CSS 创建它。

css
input + span {
  position: relative;
}

input:required + span::after {
  font-size: 0.7rem;
  position: absolute;
  content: "required";
  color: white;
  background-color: black;
  padding: 5px 10px;
  top: -26px;
  left: -70px;
}

我们将 <span> 设置为 position: relative,这样我们就可以将生成内容设置为 position: absolute,并相对于 <span> 而不是 <body> 定位它(生成内容的作用就好像它是其生成的元素的子节点,用于定位目的)。

然后,我们给生成的内容添加“required”作为内容,这是我们希望标签显示的内容,并根据需要设置样式和位置。结果如下所示(按播放按钮可在 MDN Playground 中运行示例并编辑源代码)。

根据数据是否有效来设置控件样式

表单验证的另一个真正重要、基本概念是表单控件的数据是否有效(对于数值数据,我们还可以讨论在范围内和超出范围的数据)。具有约束限制的表单控件可以根据这些状态进行定位。

:valid 和 :invalid

您可以使用 :valid:invalid 伪类来定位表单控件。需要记住的一些要点:

  • 没有约束验证的控件将始终有效,因此与 :valid 匹配。
  • 设置了 required 但没有值的控件被视为无效 — 它们将与 :invalid:required 匹配。
  • 具有内置验证的控件,例如 <input type="email"><input type="url">,在输入的数据不符合其所需的模式时(与):invalid 匹配(但当它们为空时是有效的)。
  • 当前值超出 minmax 属性指定范围限制的控件(与):invalid 匹配,但也会被 :out-of-range 匹配,稍后您将看到。
  • 还有其他一些方法可以使元素匹配 :valid/:invalid,您将在 客户端表单验证 文章中看到。但我们现在将保持简单。

让我们来看一个 :valid/:invalid 的例子。

与之前的例子一样,我们有额外的 <span> 用于生成内容,我们将用它们来提供有效/无效数据的指示。

html
<div>
  <label for="fname">First name: </label>
  <input id="fname" name="fname" type="text" required />
  <span></span>
</div>

为了提供这些指示器,我们使用以下 CSS:

css
input + span {
  position: relative;
}

input + span::before {
  position: absolute;
  right: -20px;
  top: 5px;
}

input:invalid {
  border: 2px solid red;
}

input:invalid + span::before {
  content: "✖";
  color: red;
}

input:valid + span::before {
  content: "✓";
  color: green;
}

和以前一样,我们将 <span> 设置为 position: relative,这样我们就可以相对于它们定位生成内容。然后,我们根据表单数据是有效还是无效,绝对定位不同的生成内容——分别是绿色勾号或红色叉号。为了给无效数据增加一点额外的紧迫感,我们还在无效时给输入框添加了粗红边框。

注意:我们使用 ::before 来添加这些标签,因为我们已经将 ::after 用于“required”标签。

你可以在下面试一下(按播放按钮在 MDN Playground 中运行示例并编辑源代码)。

请注意,必需的文本输入在为空时无效,但在填写内容后有效。另一方面,电子邮件输入在为空时有效,因为它不是必需的,但当它包含的内容不是正确的电子邮件地址时则无效。

在范围内和超出范围的数据

正如我们上面暗示的,还有另外两个相关的伪类需要考虑——:in-range:out-of-range。当数值输入的数据分别在指定范围内或超出指定范围时,这些伪类匹配由 minmax 指定范围限制的数值输入。

注意:数值输入类型包括 datemonthweektimedatetime-localnumberrange

值得注意的是,在范围内的输入也将被 :valid 伪类匹配,而超出范围的输入也将被 :invalid 伪类匹配。那么为什么两者都有呢?问题实际上是语义上的——超出范围是一种更具体的无效通信类型,因此您可能希望为超出范围的输入提供不同的消息,这比仅仅说“无效”对用户更有帮助。您甚至可能希望两者都提供。

让我们看一个完全做到这一点的例子,它在前一个例子的基础上,为数值输入提供超出范围的消息,并说明它们是否是必需的。

数字输入框看起来像这样:

html
<div>
  <label for="age">Age (must be 12+): </label>
  <input id="age" name="age" type="number" min="12" max="120" required />
  <span></span>
</div>

CSS 看起来像这样:

css
input + span {
  position: relative;
}

input + span::after {
  font-size: 0.7rem;
  position: absolute;
  padding: 5px 10px;
  top: -26px;
}

input:required + span::after {
  color: white;
  background-color: black;
  content: "Required";
  left: -70px;
}

input:out-of-range + span::after {
  color: white;
  background-color: red;
  width: 155px;
  content: "Outside allowable value range";
  left: -182px;
}

这与我们在 :required 示例中遇到的情况类似,不同之处在于,这里我们将适用于任何 ::after 内容的声明拆分到一个单独的规则中,并为 :required:out-of-range 状态下的不同 ::after 内容赋予了它们自己的内容和样式。您可以在此处尝试(按下播放按钮以在 MDN Playground 中运行示例并编辑源代码)。

数字输入框可能同时是必需的且超出范围,那么会发生什么呢?由于 :out-of-range 规则在源代码中出现在 :required 规则之后,因此层叠规则开始发挥作用,并显示超出范围的消息。

这效果很好 — 页面首次加载时,会显示“必需”字样,以及红叉和边框。当您输入有效的年龄(即在 12-120 范围内)时,输入框变为有效。但是,如果您随后将年龄更改为超出范围的值,则“超出允许值范围”消息将取代“必需”字样弹出。

注意:要输入无效/超出范围的值,您必须实际聚焦表单并使用键盘键入。微调按钮不允许您将值增加/减少到允许范围之外。

设置启用和禁用输入以及只读和读写样式

启用元素是可以激活的元素;它可以被选中、点击、输入等。禁用元素则无法以任何方式进行交互,甚至其数据都不会发送到服务器。

这两种状态可以使用 :enabled:disabled 进行定位。为什么禁用输入很有用?嗯,有时如果某些数据不适用于特定用户,您可能根本不想在他们提交表单时提交这些数据。一个经典的例子是送货表单 — 通常您会被问到是否要使用相同的地址进行账单和送货;如果是这样,您可以只向服务器发送一个地址,并且可以直接禁用账单地址字段。

让我们看一个实现这一点的例子。首先,HTML 是一个简单的表单,包含文本输入,以及一个复选框,用于切换账单地址的禁用状态。账单地址字段默认是禁用的。

html
<form>
  <fieldset id="shipping">
    <legend>Shipping address</legend>
    <div>
      <label for="name1">Name: </label>
      <input id="name1" name="name1" type="text" required />
    </div>
    <div>
      <label for="address1">Address: </label>
      <input id="address1" name="address1" type="text" required />
    </div>
    <div>
      <label for="zip-code1">Zip/postal code: </label>
      <input id="zip-code1" name="zip-code1" type="text" required />
    </div>
  </fieldset>
  <fieldset id="billing">
    <legend>Billing address</legend>
    <div>
      <label for="billing-checkbox">Same as shipping address:</label>
      <input type="checkbox" id="billing-checkbox" checked />
    </div>
    <div>
      <label for="name" class="billing-label disabled-label">Name: </label>
      <input id="name" name="name" type="text" disabled required />
    </div>
    <div>
      <label for="address2" class="billing-label disabled-label">
        Address:
      </label>
      <input id="address2" name="address2" type="text" disabled required />
    </div>
    <div>
      <label for="zip-code2" class="billing-label disabled-label">
        Zip/postal code:
      </label>
      <input id="zip-code2" name="zip-code2" type="text" disabled required />
    </div>
  </fieldset>

  <div><button>Submit</button></div>
</form>

现在是 CSS。这个示例最相关的部分如下:

css
input[type="text"]:disabled {
  background: #eeeeee;
  border: 1px solid #cccccc;
}

label:has(+ :disabled) {
  color: #aaaaaa;
}

我们使用 input[type="text"]:disabled 直接选中了我们想要禁用的输入框,但我们也想将相应的文本标签变灰。由于标签紧靠其输入框之前,我们使用伪类 :has 选中了它们。

最后,我们使用一些 JavaScript 来切换账单地址字段的禁用状态。

js
function toggleBilling() {
  // Select the billing text fields
  const billingItems = document.querySelectorAll('#billing input[type="text"]');

  // Toggle the billing text fields
  for (const item of billingItems) {
    item.disabled = !item.disabled;
  }
}

// Attach `change` event listener to checkbox
document
  .getElementById("billing-checkbox")
  .addEventListener("change", toggleBilling);

它使用 change 事件 来允许用户启用/禁用账单字段,并切换相关标签的样式。

您可以在下面看到示例的实际效果(按播放按钮在 MDN Playground 中运行示例并编辑源代码)。

只读和读写

:disabled:enabled 类似,:read-only:read-write 伪类针对表单输入在两种状态之间切换。与禁用输入一样,用户无法编辑只读输入。但是,与禁用输入不同,只读输入值将提交到服务器。读写意味着它们可以被编辑——这是它们的默认状态。

输入框使用 readonly 属性设置为只读。举个例子,想象一个确认页面,开发人员将前一页填写的信息发送到此页面,目的是让用户在一个地方检查所有信息,添加任何所需的最终数据,然后通过提交确认订单。此时,所有最终表单数据可以一次性发送到服务器。

让我们看看表单可能是什么样子。

HTML 片段如下所示 — 请注意 readonly 属性。

html
<div>
  <label for="name">Name: </label>
  <input id="name" name="name" type="text" value="Mr Soft" readonly />
</div>

如果你尝试实时示例,你会看到顶部的一组表单元素是不可编辑的,但是,当表单提交时,这些值会被提交。我们使用 :read-only:read-write 伪类对表单控件进行了样式设置,如下所示:

css
input:read-only,
textarea:read-only {
  border: 0;
  box-shadow: none;
  background-color: white;
}

textarea:read-write {
  box-shadow: inset 1px 1px 3px #cccccc;
  border-radius: 5px;
}

完整示例看起来像这样(按播放按钮在 MDN Playground 中运行示例并编辑源代码)。

注意::enabled:read-write 是另外两个你可能很少使用的伪类,因为它们描述了输入元素的默认状态。

单选和复选框状态 — 选中、默认、不确定

正如我们在模块的早期文章中看到的,单选按钮复选框可以被选中或未选中。但是还有其他几种状态需要考虑:

  • :default:匹配页面加载时默认选中的单选按钮/复选框(即通过在其上设置 checked 属性)。即使用户取消选中它们,它们也会匹配 :default 伪类。
  • :indeterminate:当单选按钮/复选框既非选中也非未选中时,它们被认为是不确定状态,并将匹配 :indeterminate 伪类。下面将详细解释这意味着什么。

:checked

选中时,它们将匹配 :checked 伪类。

最常见的用法是在复选框或单选按钮被选中时添加不同的样式,特别是在您使用 appearance: none; 移除了系统默认样式并希望自己重新构建样式的情况下。我们在上一篇文章中讨论 使用 appearance 设置复选框和单选按钮样式 时看到了此类示例。

回顾一下,我们“样式化单选按钮”示例中的 :checked 代码如下所示:

css
input[type="radio"]::before {
  display: block;
  content: " ";
  width: 10px;
  height: 10px;
  border-radius: 6px;
  background-color: red;
  font-size: 1.2em;
  transform: translate(3px, 3px) scale(0);
  transform-origin: center;
  transition: all 0.3s ease-in;
}

input[type="radio"]:checked::before {
  transform: translate(3px, 3px) scale(1);
  transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2);
}

你可以在这里尝试一下(按播放按钮在 MDN Playground 中运行示例并编辑源代码)。

基本上,我们使用 ::before 伪元素构建单选按钮“内圈”的样式,但为其设置 scale(0) transform。然后,我们使用 transition 使标签上生成的内容在单选按钮被选中/勾选时平滑地动画显示。使用 transform 而不是过渡 width/height 的优点是,您可以使用 transform-origin 使其从圆心生长,而不是看起来从圆的角部生长,并且由于没有更新盒模型属性值,因此没有跳跃行为。

:default 和 :indeterminate

如上所述,:default 伪类匹配页面加载时默认选中的单选按钮/复选框,即使它们未选中。这对于在选项列表中添加指示器很有用,以提醒用户默认(或起始选项)是什么,以防他们想重置他们的选择。

此外,上述单选按钮/复选框在既非选中也非未选中状态时,将匹配 :indeterminate 伪类。但这究竟意味着什么呢?处于不确定状态的元素包括:

  • 当同一名称组中的所有单选按钮都未选中时,<input/radio> 输入框。
  • 通过 JavaScript 将 indeterminate 属性设置为 true<input/checkbox> 输入框。
  • 没有值的 <progress> 元素。

这并不是你经常会用到的东西。一个用例可能是用作指示器,告诉用户在继续之前确实需要选择一个单选按钮。

让我们看几个修改后的先前示例,它们提醒用户默认选项是什么,并在单选按钮处于不确定状态时设置其标签的样式。这两个示例都具有以下 HTML 结构,用于输入:

html
<p>
  <input type="radio" name="fruit" value="cherry" id="cherry" />
  <label for="cherry">Cherry</label>
  <span></span>
</p>

对于 :default 示例,我们在中间的单选按钮输入框中添加了 checked 属性,这样它在加载时将默认选中。然后我们使用以下 CSS 对其进行样式设置:

css
input ~ span {
  position: relative;
}

input:default ~ span::after {
  font-size: 0.7rem;
  position: absolute;
  content: "Default";
  color: white;
  background-color: black;
  padding: 5px 10px;
  right: -65px;
  top: -3px;
}

这会为页面加载时最初选定的项目提供一个小的“默认”标签。请注意,这里我们使用后续同级组合器(~)而不是下一个同级组合器(+)——我们需要这样做,因为 <span> 在源顺序中并不紧跟在 <input> 之后。

请参见下面的实时结果(按播放按钮可在 MDN Playground 中运行示例并编辑源代码)。

对于 :indeterminate 示例,我们没有默认选中的单选按钮——这很重要——如果存在,那么就没有不确定状态可供样式设置。我们使用以下 CSS 为不确定状态的单选按钮设置样式:

css
input[type="radio"]:indeterminate {
  outline: 2px solid red;
  animation: 0.4s linear infinite alternate outline-pulse;
}

@keyframes outline-pulse {
  from {
    outline: 2px solid red;
  }

  to {
    outline: 6px solid red;
  }
}

这在单选按钮上创建了一个有趣的小动画轮廓,希望能表明您需要选择其中一个!

请参见下面的实时结果(按播放按钮可在 MDN Playground 中运行示例并编辑源代码)。

注意:您可以在 <input type="checkbox"> 参考页面上找到一个涉及 indeterminate 状态的有趣示例

更多伪类

还有一些其他的伪类值得关注,我们在这里没有空间详细介绍它们。让我们谈谈另外几个您应该花时间研究的。

  • :focus-within 伪类匹配已获得焦点的元素或包含已获得焦点的元素。如果您希望当其中一个输入获得焦点时整个表单以某种方式突出显示,这会很有用。
  • :focus-visible 伪类匹配通过键盘交互(而不是触摸或鼠标)获得焦点的元素——如果您希望键盘焦点与鼠标(或其他)焦点显示不同的样式,这很有用。
  • :placeholder-shown 伪类匹配显示其占位符的 <input><textarea> 元素(即 placeholder 属性的内容),因为元素的值为空。

以下内容也很有趣,但目前在浏览器中尚未得到很好的支持:

  • :blank 伪类选择空的表单控件。:empty 也匹配没有子元素的元素,例如 <input>,但它更通用——它还匹配其他空元素,如 <br><hr>:empty 具有合理的浏览器支持;:blank 伪类的规范尚未完成,因此目前任何浏览器都不支持它。
  • :user-invalid 伪类(如果支持)将类似于 :invalid,但具有更好的用户体验。如果输入获得焦点时值为有效,则用户输入数据时,如果值暂时无效,元素可能会匹配 :invalid,但只有当元素失去焦点时才会匹配 :user-invalid。如果值最初无效,则在整个焦点持续时间内,它将同时匹配 :invalid:user-invalid。与 :invalid 类似,如果值变为有效,它将停止匹配 :user-invalid

总结

我们对与表单输入相关的 UI 伪类的介绍到此结束。继续玩它们,创造一些有趣的表单样式!接下来,我们将转向一些不同的东西——客户端表单验证