:has() 擬似クラスを使って CSS だけで必須フィールドのラベルに * マーカーを付ける

フォームの表現でよく見られる、必須フィールドとなるラベルに * のようなマーカーを付ける方法について、 CSS のみで実装する方法を紹介します。

## 注意

[:has()](https://developer.mozilla.org/ja/docs/Web/CSS/:has) は現在 (2022/04/23) 時点での対応ブラウザは Safari 15.4 以降のみなので、お使いのブラウザでは動作しない可能性があります。

https://caniuse.com/css-has

## そもそも `:has()` とは

これまでの CSS セレクタでは不可能だった、自分の子要素や後に続く要素から判別することができる CSS の擬似クラスです。
例えば、 `<img>` 要素を直接含む `<div>` を指定する場合、以下のように書くことが出来ます。

```css
div:has(> img) {
}
```

## 実現方法

ラベル付きの入力フィールドについて、よく使われる構造に対してそれぞれ二つずつ実装方法を紹介します。
例では `<input>` のみとしていますが、同じようなやり方で別のフィールドタグについても対応できます。

### `<label>` と `<input>` が隣接する場合

```html
<div>
  <label></label>
  <input>
</div>
```

上のような構造の場合、「`div` の子要素で隣接要素に `input:required` を持つ `label`」と「子要素に `input:required` を持つ `div` の子要素 `label`」という指定方法で、それぞれ以下のように指定することができます。

```css
div > label:has(+ input:required)::after {
  content: "*";
}

div:has(> input:required) > label::after {
  content: "*";
}
```

### `<input>` を `<label>` の内側に入れる場合

```html
<label>
  <span></span>
  <input>
</label>
```

上のような構造の場合も同様に、「`div` の子要素で隣接要素に `input:required` を持つ `span`」と「子要素に `input:required` を持つ `label` の子要素 `span`」という指定方法で、それぞれ以下のように指定することができます。

```css
label > span:has(+ input:required)::after {
  content: "*";
}

label:has(> input:required) > span::after {
  content: "*";
}
```

※ [::after](https://developer.mozilla.org/ja/docs/Web/CSS/::after) 擬似要素を使っている都合上、フィールドのラベルを `<span>` で囲うような構造にしています。

## サンプルコード

上の各方法で実装したサンプルコードが以下です。
セレクタの指定をいくつか変えています。

```html: Sample Form
<form>
  <div class="labeled-input labeled-input--adjacent">
    <label>Name</label>
    <input type="text" required>
  </div>
  <div class="labeled-input labeled-input--descendant">
    <label>Email</label>
    <input type="email" required>
  </div>
  <label class="label-with-input label-with-input--adjacent">
    <span>Tel</span>
    <input type="tel" required>
  </label>
  <label class="label-with-input label-with-input--descendant">
    <span>Birthday</span>
    <input type="date" required>
  </label>
  <div>
    <input type="submit" value="Submit">
  </div>
</form>

<style>
:root {
  background-color: #fcfcfc;
}

form {
  width: 160px;
  margin: 12px;
  padding: 12px;
  border: solid thin grey;
}

form > *:not(:first-child) {
  margin-top: 4px;
}

.labeled-input > label,
.label-with-input,
.label-with-input > span {
  display: block;
}

.labeled-input.labeled-input--adjacent
  > label:has(+ input:required)::after {
  content: "*";
  color: red;
}

.labeled-input.labeled-input--descendant:has(> input:required)
  > label::after {
  content: "*";
  color: blue;
}

.label-with-input.label-with-input--adjacent
  > span:has(+ input:required)::after {
  content: "*";
  color: green;
}

.label-with-input.label-with-input--descendant:has(> input:required)
  > span::after {
  content: "*";
  color: orange;
}
</style>
```

実際の Safari での表示が以下のようになります。
それぞれ `required` となるラベルの右に `*` マーカーがついているのが確認できます。

![sample_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158991/a7e46d85-77ed-6a4e-8d91-040796a837ce.png)

## Vuetify の場合

おまけで仕事でよく使う [Vuetify](https://vuetifyjs.com/ja/) でも試してみました。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="xxpQgyp" data-user="aster-mnch" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/aster-mnch/pen/xxpQgyp">
  Mark required fields with * in Vuetify</a> by Kitagawa (<a href="https://codepen.io/aster-mnch">@aster-mnch</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## おわりに

最近各ブラウザでの `:has()` 擬似クラス対応が進んでいるため、今後はいろいろなサイトで使われていくかもしれません。
今回紹介した内容以外にもさまざまな使い方が以下のサイトで紹介されているので、気になった方は是非試してみてください。

https://coliss.com/articles/build-websites/operation/css/has-pseudo-class.html