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 偽類的介紹到此結束。繼續玩它們,創造一些有趣的表單樣式!接下來,我們將轉向一些不同的東西——客戶端表單驗證