UI 伪类

在之前的文章中,我们以一般的方式介绍了各种表单控件的样式。这包括一些伪类的用法,例如,使用:checked仅在选中复选框时才将其作为目标。在本文中,我们探讨了可用于在不同状态下为表单设置样式的不同 UI 伪类。

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

我们有哪些可用的伪类?

与表单相关的原始伪类(来自CSS 2.1)是

  • :hover:仅当鼠标指针悬停在元素上时才选择该元素。
  • :focus:仅当元素获得焦点时才选择该元素(例如,通过键盘选项卡)。
  • :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 (include 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: 1px solid black;
}

input:optional {
  border: 1px solid silver;
}

必填控件将具有黑色边框,可选控件将具有银色边框,如下所示

您还可以尝试在不填写表单的情况下提交表单,以查看浏览器默认提供的客户端验证错误消息。

上面的表单还不错,但也不算好。首先,我们仅使用颜色来指示必填项与可选项状态,这对色盲人士来说不是很好。其次,网络上关于必填项状态的标准约定是用星号 (*) 或与相关控件关联的“必填”字样。

在下一节中,我们将查看一个使用: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>

这里遇到的直接问题是,由于输入和标签都设置了width: 100%,因此 span 会落到输入下方的新行上。为了解决这个问题,我们将父<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>对其进行定位(就定位而言,生成的内容的行为就像它是其生成元素的子节点)。

然后,我们为生成的内容提供内容“必填”,这就是我们希望标签显示的内容,并根据需要对其进行样式设置和定位。结果如下所示。

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

表单验证中另一个真正重要、基本的概念是表单控件的数据是否有效(在数值数据的情况下,我们还可以讨论数据是否在范围内或超出范围)。具有约束限制的表单控件可以根据这些状态作为目标。

:valid 和 :invalid

您可以使用:valid:invalid伪类将表单控件作为目标。一些值得注意的要点

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

让我们深入了解一个简单的:valid/:invalid示例(查看valid-invalid.html以获取实时版本,并查看源代码)。

与之前的示例一样,我们有额外的<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用于“必填”标签。

您可以在下面尝试。

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

范围内的和范围外的的数据

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

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

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

让我们来看一个正好这样做的例子。我们的out-of-range.html 演示(另请参阅源代码)建立在之前的示例之上,为数字输入提供超出范围的消息,以及它们是否为必填项。

数字输入如下所示

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内容提供了它们自己的内容和样式。您可以在此处尝试。

数字输入有可能同时是必填的和超出范围的,那么在这种情况下会发生什么?由于: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="pcode1">Zip/postal code: </label>
      <input id="pcode1" name="pcode1" 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="pcode2" class="billing-label disabled-label">
        Zip/postal code:
      </label>
      <input id="pcode2" name="pcode2" type="text" disabled required />
    </div>
  </fieldset>

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

现在进入 CSS 代码。此示例中最相关的部分如下所示

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

.disabled-label {
  color: #aaa;
}

我们使用input[type="text"]:disabled直接选择了要禁用的输入,但我们还想将相应的文本标签灰显。这些并不容易选择,因此我们使用了类来为它们提供该样式。

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

js
// Wait for the page to finish loading
document.addEventListener(
  "DOMContentLoaded",
  () => {
    // Attach `change` event listener to checkbox
    document
      .getElementById("billing-checkbox")
      .addEventListener("change", toggleBilling);
  },
  false,
);

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

  // Toggle the billing text fields and labels
  for (let i = 0; i < billingItems.length; i++) {
    billingItems[i].disabled = !billingItems[i].disabled;

    if (
      billingLabels[i].getAttribute("class") === "billing-label disabled-label"
    ) {
      billingLabels[i].setAttribute("class", "billing-label");
    } else {
      billingLabels[i].setAttribute("class", "billing-label disabled-label");
    }
  }
}

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

您可以在下面看到示例的实际效果(还可以在此处查看其在线演示,并查看源代码)。

只读和读写

:disabled:enabled 类似,:read-only:read-write 伪类针对表单输入切换的两种状态。只读输入将其值提交到服务器,但用户无法编辑它们,而读写表示可以编辑它们——它们默认的状态。

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

让我们看看表单可能是什么样子(有关在线示例,请参阅readonly-confirmation.html;另请参阅源代码)。

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 #ccc;
  border-radius: 5px;
}

完整示例如下所示

注意:鉴于:enabled:read-write 描述了输入元素的默认状态,因此您可能很少使用这两个伪类。

单选按钮和复选框状态 - 已选中、默认、不确定

正如我们在模块中较早的文章中看到的那样,单选按钮复选框 可以选中或取消选中。但还有几个其他状态需要考虑

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

:checked

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

最常见的用法是在选中复选框或单选按钮时为其添加不同的样式,在您使用appearance: none; 去除了系统默认样式并想要自己构建样式的情况下。在上一篇文章中,当我们讨论在单选按钮/复选框上使用appearance: none 时,我们看到了这方面的示例。

概括地说,我们样式化的单选按钮 示例中的: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);
}

您可以在此处试用。

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

:default 和 :indeterminate

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

此外,上面提到的单选按钮/复选框在既未选中也未取消选中状态时将与:indeterminate 伪类匹配。但这意味着什么呢?不确定的元素包括

  • <input/radio> 输入,当同一命名组中的所有单选按钮都未选中时
  • <input/checkbox> 输入,其indeterminate 属性通过 JavaScript 设置为true
  • <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> 之后。

请参见下面的在线结果。

注意:您也可以在 GitHub 上找到在线示例,网址为radios-checked-default.html(另请参阅源代码)。

对于: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;
  }
}

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

请参见下面的在线结果。

注意: 您也可以在 GitHub 上找到此示例的实际演示,网址为 radios-checked-indeterminate.html(另请参阅 源代码)。

注意: 您可以在 <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 伪类的介绍。继续使用它们,并创建一些有趣的表单样式!接下来,我们将转向其他内容—— 客户端表单验证

高级主题