【モダンCSS】:hasと:notでJavaScriptレスなUIを実現する + コードスニペット付き

## はじめに
JavaScriptを使ってUI状態を管理する代わりに、CSSの`:has`と`:not`を使うことで、よりシンプルな実装が可能になります！
この記事では、これらの擬似クラスの基本から実践的な使い方まで、具体的なコード例を交えながら紹介します。

## CSSの:hasと:notとは

### :has() 擬似クラス

::: note
CSSに関する調査である「[State of CSS 2024](https://2024.stateofcss.com/ja-JP/features/#all_features)」によると`:has`擬似クラスは、2024年の1年間で2番目に多く使用されたCSS機能です。それだけ注目されている機能ということの表れですね。
:::

`:has()`は、「関係擬似クラス」と呼ばれるCSSセレクターの一つです。親要素や兄弟要素の中に特定の要素が存在するかどうかを判定できます。

#### 特徴
- 親要素から子要素の状態を確認できる
- 複数の条件を組み合わせることが可能
- [モダンブラウザ](https://caniuse.com/?search=has)（Chrome、Firefox、Safari、Edge最新版）でサポート

#### 基本的な書き方
```style.css
/* 基本構文 */
要素:has(セレクター) {
  /* スタイル */
}

/* imgを持つaタグを選択 */
a:has(img) {
  padding: 10px;
}

/* 必須項目を含むフォームグループを選択 */
.form-group:has([required]) {
  background: #f8f8f8;
}
```

### :not() 擬似クラス

`:not()`は、「否定擬似クラス」と呼ばれるCSSセレクターです。指定したセレクターに一致しない要素を選択できます。

#### 特徴
- 特定の条件を満たさない要素を選択できる
- 複数のセレクターを組み合わせることが可能
- ほぼすべての[モダンブラウザ](https://caniuse.com/css-not-sel-list)でサポート

#### 基本的な書き方
```style.css
/* 基本構文 */
要素:not(セレクター) {
  /* スタイル */
}

/* 最後の要素以外を選択 */
li:not(:last-child) {
  border-bottom: 1px solid #ddd;
}

/* 特定のクラスを持たない要素を選択 */
div:not(.special) {
  background: #fff;
}
```

### hasとnotの組み合わせ

両方の擬似クラスを組み合わせることで、より細かい条件指定が可能になります：

```style.css
/* 画像を持たず、特定のクラスも持たない要素 */
.card:not(:has(img)):not(.special) {
  padding: 20px;
}

/* 必須項目を持つが送信ボタンは含まないフォーム */
form:has([required]):not(:has(button[type="submit"])) {
  border: 2px solid #ff0000;
}
```

### 使用時の注意点

1. パフォーマンスへの影響
   - 複雑なセレクターの組み合わせは避ける
   - 不必要に深いセレクターを作らない

2. ブラウザサポート
   - `:has`は比較的新しい機能のため、古いブラウザでは動作しない
   - 必要に応じて代替手段を用意する

3. 可読性
   - 複雑な条件は避け、シンプルに保つ
   - 適切なクラス名と組み合わせる

## 実践的な使用例
:::note
UI集の内容は随時更新します。
:::

### 1. シンプルなアコーディオン
![スクリーンショット 2024-12-05 2.17.38.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624902/ca38dc0d-2733-8d2a-1ff5-152c494f753c.jpeg)

```index.html
<div class="accordion">
  <!-- チェックボックスは非表示にします -->
  <input type="checkbox" id="acc1" class="accordion-checkbox">
  
  <!-- ラベルをクリックでトグル -->
  <label for="acc1" class="accordion-header">
    開閉ボタン
  </label>
  
  <!-- アコーディオンの中身 -->
  <div class="accordion-content">
    <p>アコーディオンの中身です</p>
  </div>
</div>
```

```style.css
/* チェックボックスを非表示 */
.accordion-checkbox {
  display: none;
}

/* 閉じている時のコンテンツ */
.accordion-content {
  display: none;
  background-color: #f5f5f5;
  padding: 15px;
}

/* 開いている時のコンテンツ */
.accordion:has(.accordion-checkbox:checked) .accordion-content {
  display: block;
}

/* ヘッダーの装飾 */
.accordion-header {
  background-color: #e0e0e0;
  padding: 10px;
  cursor: pointer;
  display: block;
}

/* ホバー時の装飾 */
.accordion-header:hover {
  background-color: #d0d0d0;
}
```

### 2. フォームのバリデーション表示
![スクリーンショット 2024-12-05 2.17.50.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624902/a9292e83-69e5-c9b3-88ae-0366cef00cd6.jpeg)

```index.html
<div class="form-group">
  <label for="email">メールアドレス</label>
  <input type="email" id="email" class="form-input" required>
  <p class="error-message">正しいメールアドレスを入力してください</p>
</div>
```

```style.css
/* 入力欄の基本スタイル */
.form-input {
  border: 2px solid #ddd;
  padding: 8px;
  border-radius: 4px;
  width: 100%;
}

/* エラーメッセージは通常非表示 */
.error-message {
  display: none;
  color: #ff4444;
  font-size: 14px;
  margin-top: 5px;
}

/* 無効な入力がある場合のスタイル */
.form-group:has(input:invalid) .form-input {
  border-color: #ff4444;
}

/* 無効な入力がある場合にエラーメッセージを表示 */
.form-group:has(input:invalid) .error-message {
  display: block;
}

/* 有効な入力の場合のスタイル */
.form-group:has(input:valid) .form-input {
  border-color: #44cc44;
}
```

### 3. カード一覧のレイアウト制御
![スクリーンショット 2024-12-05 2.18.08.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624902/96fbc3e1-845c-57ce-1cf8-c88ad7e5ed33.jpeg)

```index.html
<div class="card-container">
  <!-- 画像付きカード -->
  <div class="card">
    <img src="example.jpg" alt="説明">
    <h3>タイトル</h3>
    <p>説明文</p>
  </div>
  
  <!-- 画像なしカード -->
  <div class="card">
    <h3>タイトル</h3>
    <p>説明文</p>
  </div>
</div>
```

```style.css
.card-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
  padding: 20px;
}

.card {
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* 画像付きカードのスタイル */
.card:has(img) {
  /* 画像がある場合は1列分 */
  grid-column: span 1;
}

/* 画像なしカードのスタイル */
.card:not(:has(img)) {
  /* 画像がない場合は2列分使用 */
  grid-column: span 2;
  padding: 20px;
}

/* カード内の画像スタイル */
.card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 8px 8px 0 0;
}

/* カード内のテキストエリア */
.card h3 {
  margin: 15px 0;
  padding: 0 15px;
}

.card p {
  padding: 0 15px 15px;
  color: #666;
}
```

### 4. タブ切り替え
![スクリーンショット 2024-12-05 2.18.23.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624902/a9be1ce5-2ac6-42e3-3eea-caf78c092a47.jpeg)

```index.html
<div class="tabs">
  <div class="tab-buttons">
    <input type="radio" name="tabs" id="tab1" checked>
    <label for="tab1">タブ1</label>
    
    <input type="radio" name="tabs" id="tab2">
    <label for="tab2">タブ2</label>
  </div>
  
  <div class="tab-contents">
    <div class="tab-content" data-tab="tab1">
      <p>タブ1の内容</p>
    </div>
    <div class="tab-content" data-tab="tab2">
      <p>タブ2の内容</p>
    </div>
  </div>
</div>
```

```style.css
.tabs {
  max-width: 600px;
  margin: 20px auto;
}

/* ラジオボタンを非表示 */
.tab-buttons input[type="radio"] {
  display: none;
}

/* タブボタンのスタイル */
.tab-buttons label {
  display: inline-block;
  padding: 10px 20px;
  background: #eee;
  cursor: pointer;
  border-radius: 4px 4px 0 0;
}

/* 選択されているタブのスタイル */
.tabs:has(#tab1:checked) label[for="tab1"],
.tabs:has(#tab2:checked) label[for="tab2"] {
  background: #007bff;
  color: white;
}

/* タブコンテンツの基本スタイル */
.tab-content {
  display: none;
  padding: 20px;
  background: white;
  border: 1px solid #ddd;
}

/* 選択されているタブのコンテンツを表示 */
.tabs:has(#tab1:checked) .tab-content[data-tab="tab1"],
.tabs:has(#tab2:checked) .tab-content[data-tab="tab2"] {
  display: block;
}
```

## 実装時の注意点

1. コードの見通しを良くする
   - セレクタは短くシンプルに
   - 複雑な条件は避ける
   - クラス名は役割がわかりやすいものに

2. ブラウザ対応
   - 新しいブラウザでは動作しますが、古いブラウザでは動作しない可能性があります
   - 必要に応じて代替の実装を用意しましょう
    ```style.css
    /* 代替実装の例 */
    @supports not selector(:has(*)) {
      /* :hasが使えない環境向けの代替スタイル */
      .card-no-image {
        grid-column: span 2;
        padding: 20px;
      }
    }
    ```

3. 使用場面を考える
   - ウェブアクセシビリティを考慮すると、セマンティックなHTML要素やJavaScript処理を使用すべき場面も多いので、`:has` `:not`でどの範囲まで状態管理を行うかはあらかじめ考慮する必要がある

## まとめ

- `:has`と`:not`を使うことで、JavaScriptのコードを減らせる
- シンプルな状態管理なら、CSSだけで実装可能
- 適切に使えば、コードがわかりやすくなる
- ブラウザのサポート状況に注意

これらのコード例は、コピー＆ペーストで使えるように、最小限の依存関係で動作するように設計しています。実際のプロジェクトに合わせて、適宜カスタマイズしてご利用ください。