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 偽類的介紹。繼續使用它們,並建立一些有趣的表單樣式!接下來,我們將轉向其他內容—— 客戶端表單驗證

高階主題