タグアーカイブ 状態管理

CSS疑似クラスでJavaScript代用、状態監視の全貌とevent-triggerの将来像

CSS疑似クラスでJavaScript代用、状態監視の全貌とevent-triggerの将来像

CSSは状態を追跡するための仕組みを着実に増やしている。かつてはJavaScriptでしか扱えなかったUIの変化を、疑似クラスだけで表現できる場面が広がっているのだ。

:hoverや:focusといった基本的なものから、:autofillや:volume-lockedのような新しい疑似クラスまで、その数は増え続けている。これらをJavaScriptイベントリスナーと比較しながら整理すると、CSSの設計思想がより明確に見えてくる。

ポインター系疑似クラス、UI反応の基本

ポインター系疑似クラス、UI反応の基本

マウスやタッチ操作に反応する疑似クラスは、ウェブデザインの基本だ。:hoverはpointerenterからpointerleaveまでの継続的な状態を表し、:activeは押下中の瞬間を捉える。これらは単なる一時的な出来事ではなく、ブラウザが認識する「状態」として設計されている。

:hoverと:activeの本質

CSS-Tricksの著者Carlo Daniele氏は、疑似クラスが「イベントではなく状態を追跡する」点を強調している。:hoverはpointerenterとpointerleaveという2つのJavaScriptイベントの間を継続的に監視する状態であり、:activeはpointerdownとpointerupの間の押下状態だ。この違いを理解すると、CSSとJavaScriptの適切な役割分担が明確になる。

通常状態(Before)
ボタン
ホバー状態(After)
ボタン
通常時  ホバー時

上記のデモは静的な比較だが、実際の:hoverはカーソルが要素に乗っている間だけ継続する。JavaScriptではpointerenterとpointerleaveを個別に監視する必要があるが、CSSなら1行のセレクタで完結する。この簡潔さがCSSの強みだ。

pointer-eventsプロパティで反応を制御する

pointer-events: noneを指定すると、その要素はポインター関連のイベントを一切発火しなくなる。CSSで直接イベントの発生を抑制できる点は、JavaScriptでpreventDefaultを行うのとは異なる設計思想だ。装飾的なオーバーレイや無効化されたボタンなど、視覚的に存在するが操作対象ではない要素に有効な手法である。

フォーカス系疑似クラス、アクセシビリティの根幹

フォーカス系疑似クラス、アクセシビリティの根幹

フォーカス管理はユーザビリティとアクセシビリティの両面で重要だ。CSSの疑似クラスは、JavaScriptのfocus/blurイベントよりも直感的にフォーカス状態をスタイリングできる。

:focusと:focus-visibleの使い分け

:focusは要素がフォーカスを受け取った瞬間に発動するが、マウス操作でもキーボード操作でも一律に適用される。一方、:focus-visibleはブラウザのヒューリスティック(経験則)に基づき、フォーカスインジケーターを表示すべきかどうかを判断する。キーボード操作時にはアウトラインを表示し、マウスクリック時には非表示にする、といった制御がCSSだけで可能だ。

JavaScriptで同等の処理を書く場合、:focus-visible疑似クラスをmatchMediaで問い合わせる必要があるが、CSSならセレクタに:focus-visibleを追加するだけで済む。コード量の差は一目瞭然である。

:focus-withinと:has()の比較

:focus-withinは子孫要素がフォーカスを持っている場合に、親要素をスタイリングできる。フォーム全体をハイライトしたいときに便利だ。:has(:focus)も機能的には同等だが、:has()はより汎用的な「条件付き親セレクタ」として設計されており、フォーカス以外の条件にも対応できる。どちらを使うかは文脈次第だが、フォーカス特化の:focus-withinのほうが意図は明確になる。

フォームコンテナ(:focus-within未適用)
お名前
入力欄(未フォーカス)
フォームコンテナ(:focus-within適用)
お名前
入力欄(フォーカス中)

このように、子要素のフォーカス状態を親要素のスタイルに反映させる仕組みは、JavaScriptでDOMトラバーサルを書くよりも圧倒的にシンプルだ。

フォーム系疑似クラス、バリデーションをCSSで扱う

フォーム系疑似クラス、バリデーションをCSSで扱う

フォームの入力チェックはウェブ開発の定番だが、CSSの疑似クラスを使えば、視覚的なフィードバックの大部分をJavaScriptなしで実装できる。:validや:invalidはもちろん、:user-validや:autofillといった新しい疑似クラスも登場している。

:checkedで切り替えUIを実装する

:checkedはチェックボックスやラジオボタンの選択状態を追跡する。JavaScriptのchangeイベントと異なり、CSSセレクタとしてスタイルに直接結びつく。CSS-Tricksの著者Carlo Daniele氏も指摘しているように、CSS疑似クラスは多くの場合、2つのJavaScriptイベントの間の状態を表現するが、時には条件分岐ロジックそのものを代替する。

未チェック状態
同意する
送信ボタンは無効
チェック状態
同意する
送信可能

この切り替えはJavaScriptのchangeイベントでcheckedプロパティを判定するのと同等だが、CSSではスタイルシート内のセレクタで完結する。コードの見通しが良くなる点が大きな利点だ。

:user-validと:autofill、ユーザー体験を高める新しい疑似クラス

:validや:invalidはページ読み込み直後から評価されるため、未入力の必須フィールドが即座に赤く表示される問題があった。:user-validと:user-invalidは、ユーザーが実際に値を入力しフォーカスを外すまで評価を遅延させる。JavaScriptのchangeイベントに近い挙動だ。

:autofillはブラウザの自動入力機能を検出するJavaScriptイベントが存在しない中で、CSSだけで自動入力されたフィールドをスタイリングできる貴重な手段である。パスワードマネージャーによる自動入力を視覚的に識別したい場合に実用的だ。

バリデーション状態の比較
:invalid 読み込み直後に即時評価(未入力でも赤くなる)
:user-invalid ユーザー操作後まで評価を遅延(実用的)
:autofill ブラウザ自動入力を検出(JSイベント無し)
即時評価  遅延評価  自動入力検出

JavaScriptで同等の処理を実装する場合、checkValidity()メソッドやValidityStateオブジェクトを使うことになる。フォーム送信時に全体を検証するケースではJavaScriptが適しているが、入力中のリアルタイムフィードバックはCSSの疑似クラスに任せるほうが合理的だ。

メディア要素疑似クラス、動画と音声の状態をスタイリングする

メディア要素疑似クラス、動画と音声の状態をスタイリングする

HTML5の<audio>要素や<video>要素は、これまでJavaScriptで状態を監視しなければカスタムコントロールを作成できなかった。しかし、CSSのメディア要素疑似クラスが整備されつつあり、状況は変わりつつある。これらの疑似クラスはInterop 2026の対象にもなっており、ブラウザ間の相互運用性の向上が期待されている。

メディア疑似クラスとJSイベントの対応
CSS :buffering JS waiting
CSS :muted JS volumechange
CSS :playing JS playing (playではない)
CSS :stalled JS stalled
CSS :volume-locked JS 専用イベントなし(要ダミー要素)
CSS疑似クラス  対応するJavaScriptイベント

:volume-lockedは特に興味深い。JavaScriptで音量ロックを検出するには、ダミーのvideo要素を生成してvolumeを設定し、その値が反映されたかどうかを確認するという回りくどい方法が必要になる。CSSなら:volume-locked疑似クラスひとつで完結する。ブラウザ側の制約をCSSが抽象化してくれる好例だ。

インタラクティブ要素疑似クラス、ポップオーバーとダイアログの制御

インタラクティブ要素疑似クラス、ポップオーバーとダイアログの制御

近年HTMLに追加された<dialog>要素やポップオーバー機能は、CSSの疑似クラスと組み合わせることで真価を発揮する。:popover-open、:open、:modal、:fullscreenといった疑似クラスは、JavaScriptのtoggleイベントに頼らずにUIの状態をスタイリングできる。

閉じた状態(:open非該当)
ダイアログ(非表示)
開くボタン
開いた状態(:open該当)
ダイアログ(表示中)
閉じるボタン
非表示  表示中

JavaScriptで同様の処理を書く場合、toggleイベントを監視してから要素のopenプロパティをチェックする2段階の処理が必要になる。CSSの疑似クラスなら、スタイルシート内で直感的に記述できる。モーダルダイアログが開いているときに背景を暗くする処理も、:modal疑似クラスと::backdrop疑似要素の組み合わせで表現可能だ。

event-trigger、CSSに真のイベントリスナーが到来する未来

event-trigger、CSSに真のイベントリスナーが到来する未来

CSS-Tricksの著者Carlo Daniele氏が紹介しているAnimation Triggers仕様のevent-triggerは、現時点ではどのブラウザも未実装だが、CSSの状態監視を次の段階に引き上げる提案だ。これは従来の疑似クラスとは異なり、JavaScriptのイベントリスナーに近い働きをする。

event-triggerの基本構文

event-triggerは、CSSアニメーションを特定のイベントに結びつける仕組みである。event-trigger-nameでアニメーションの識別子を定義し、event-trigger-sourceで発火条件となるイベント(click、touch、dblclick、keypressなど)を指定する。アニメーションは通常その場で再生されるのではなく、イベントが発生するまで待機する。

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

button {    
  /* クリック時に --event アニメーションを発火 */
  event-trigger: --event click;
}

div {
  /* --event が発火したらアニメーションを前方再生 */
  animation-trigger: --event play-forwards;
  animation: fade-in 300ms both;
}
STEP 1 ボタンに event-trigger を設定
STEP 2 クリックイベントで –event が発火
STEP 3 animation-trigger がアニメーションを再生
STEP 4 対象要素がフェードインアニメーションを実行

この流れは、従来のCSS疑似クラスが「状態」を追跡していたのに対し、明らかに「イベント」の発生をトリガーとしている点が新しい。clickイベントは元に戻せない不可逆的な出来事だが、interestイベントのように出入りがあるイベントの場合は、ステートフルな双方向トリガーも定義できる。

ステートレスとステートフルの2種類のトリガー

event-triggerには2つのモードが想定されている。ステートレストリガーはclickのような一度きりのイベント向けで、アニメーションは一方向にのみ再生される。ステートフルトリガーはinterestのような持続的な関心を表すイベント向けで、イベントの開始と終了に応じてアニメーションを前後に再生できる。

/* ステートフルトリガーの例 */
button {    
  event-trigger: --event interest / interest;
}

div {
  animation-trigger: --event play-forwards play-backwards;
  animation: fade-in 300ms both;
}

この構文では、interestの開始時にアニメーションを前方再生し、interestの終了時に逆再生する。ホバーでメニューがスライドインし、カーソルが離れるとスライドアウトするようなUIを、JavaScriptのイベントリスナーなしで実装できる可能性がある。

ステートレストリガー(click)
イベント クリック アニメーション 前方再生のみ
ステートフルトリガー(interest)
開始イベント 関心を持つ 前方再生
終了イベント 関心を失う 逆再生
ステートレス  ステートフル

この仕様が実用化されれば、CSSのみで完結するUIコンポーネントの幅は大幅に広がるだろう。ただし、Carlo Daniele氏自身も記事内で触れているように、仕様は現在編集中であり、今後のドラフトで構成が大きく変わる可能性がある。現時点では構想段階の提案として捉えておくのが適切だ。

この記事のポイント

  • CSS疑似クラスはJavaScriptイベントの代替ではなく、状態を監視する独自のレイヤーとして進化している
  • :hoverや:focusのような基本疑似クラスから、:autofillや:volume-lockedのような新しい疑似クラスまで、対応範囲は拡大中
  • メディア要素疑似クラスはInterop 2026の対象であり、ブラウザ間の相互運用性が今後向上する
  • event-trigger仕様はCSSに真のイベントリスナーをもたらす提案だが、現時点では未実装であり将来の動向に注目すべき
  • CSSとJavaScriptは競合するものではなく、適材適所で組み合わせることで効率的なUI開発が可能になる
CSSだけで多階層の状態を管理するラジオボタン・ステートマシンの実装手法

CSSだけで多階層の状態を管理するラジオボタン・ステートマシンの実装手法

Web制作における状態管理は、多くの場合JavaScriptの役割だと考えられている。しかし、純粋に視覚的なUIの変化であれば、CSSだけで完結させるアプローチが非常に有効な場合がある。

パネルの開閉やアイコンの形態変化、カードの反転といった「表示上の状態」をCSSで管理することで、JavaScriptのオーバーヘッドを削減し、プレゼンテーション層に近い場所でロジックを保持できる。この記事では、従来のチェックボックスハックを進化させた「ラジオボタン・ステートマシン」という手法について詳しく解説する。

この手法をマスターすると、複雑な多段階のUI遷移もHTMLとCSSのみで堅牢に実装できるようになる。技術に詳しい同僚が教えるような感覚で、具体的なコード例を交えながらその仕組みを紐解いていこう。

CSSによる状態管理の新しいアプローチ

CSSによる状態管理の新しいアプローチ

Webサイトのインタラクションにおいて、すべての状態変化にJavaScriptが必要なわけではない。ビジネスロジックやデータの永続化が絡まない、純粋な表示の切り替えであれば、CSSの機能を活用したほうがスマートな解決策になることが多い。

JavaScriptを使わない選択肢

JavaScriptは強力だが、依存しすぎるとコードの複雑さが増し、パフォーマンスにも影響を与える。例えば、ダークモードの切り替えやタブメニューの制御をCSSで行うと、ページの読み込み直後から即座に反応し、スクリプトの実行待ちによる遅延が発生しない。これは、ユーザー体験の向上に直結する重要なポイントだ。

従来のチェックボックスハックとその限界

CSSで状態を管理する古典的な手法として「チェックボックスハック」がある。これは、非表示にしたチェックボックスの :checked 擬似クラスを利用し、隣接する要素のスタイルを変更するテクニックだ。しかし、この方法には「オンかオフか」という2つの状態しか持てないという明確な限界がある。3つ以上の状態を切り替えたい場合には、別の工夫が必要になる。

ラジオボタン・ステートマシンの基本構造

ラジオボタン・ステートマシンの基本構造

2つの状態しか持てないチェックボックスに対し、複数の選択肢から1つだけを選べるラジオボタンを利用するのが「ラジオボタン・ステートマシン」の核心だ。ラジオボタンは同じ name 属性を持つグループ内で排他的に動作するため、これを「現在の状態」として利用する。

相互排他的な状態を作る仕組み

まず、複数のラジオボタンを用意し、それぞれに異なる状態を割り当てる。例えば「状態A」「状態B」「状態C」の3つがある場合、HTML構造は以下のようになる。ここで重要なのは、ラジオボタンを display: none で消すのではなく、アクセシビリティを考慮した方法で隠すことだ。

<div class="state-container">
  <input type="radio" name="ui-state" id="state-1" checked>
  <input type="radio" name="ui-state" id="state-2">
  <input type="radio" name="ui-state" id="state-3">
  
  <div class="content">
    <!-- ここに状態に応じて変化する要素を配置 -->
  </div>
</div>

ボタンの見た目をカスタマイズする

ラジオボタンそのものをUIのボタンとして機能させるには、appearance: none を使用してデフォルトのスタイルを解除する。これにより、ラジオボタンをあたかも普通のボタンやタブのようにスタイリングできるようになる。疑似要素の ::after などを使ってラベルテキストを表示すれば、HTMLタグを最小限に抑えたまま、インタラクティブな要素が完成する。

状態切り替えデモ(簡易版)

状態1(選択中)
状態2
状態3
現在は「状態1」のコンテンツが表示されています
選択されている状態  選択されていない状態

このデモはラジオボタンの排他的な性質を利用した状態遷移を視覚化したものだ。実際の実装では、クリックするたびに :checked が移動し、それに応じて下のコンテンツが切り替わる仕組みになる。

循環型と非循環型のフロー制御

循環型と非循環型のフロー制御

ステートマシンを構築する際、ユーザーがどのように状態間を移動するかを設計する必要がある。すべての状態をループさせる「循環型」と、最初から最後まで順番に進む「非循環型(リニア型)」の2パターンが主に使われる。

次の状態へ進むシーケンシャルな遷移

例えば、クリックするたびに「進む」だけのUIを作る場合、現在の状態の「次」にあるラジオボタンだけを表示させるテクニックが使える。CSSの隣接兄弟結合子 + を活用し、input:checked + input というセレクタを使えば、現在選択されている要素の直後にある要素だけにスタイルを適用できる。

input[name="state"] {
  position: fixed;
  opacity: 0;
  pointer-events: none;
}

/* 現在チェックされているものの次にあるボタンだけを表示する */
input[name="state"]:checked + input[name="state"] {
  position: relative;
  opacity: 1;
  pointer-events: all;
  appearance: none;
  /* ボタンとしてのスタイル */
}

前に戻る双方向フローの実装

「戻る」ボタンも実装したい場合は、最新のCSS擬似クラスである :has() が威力を発揮する。input:has(+ input:checked) というセレクタを使えば、「次にチェックされている要素がある場合の、自分自身」をターゲットにできる。これにより、進むボタンと戻るボタンの両方をCSSだけで制御可能になる。

カスタムプロパティと計算式の活用

カスタムプロパティと計算式の活用

ラジオボタン・ステートマシンの真価は、CSSカスタムプロパティ(変数)と組み合わせたときに発揮される。各状態に対して直接スタイルを記述するのではなく、変数の値だけを書き換える手法だ。

状態を変数として一括管理する

例えば、状態ごとに要素の位置や色を変えたい場合、各状態の :checked 時に --state-index のような変数の値を変更する。これにより、各コンポーネント側ではその変数を参照するだけで済み、コードの重複を劇的に減らすことができる。

.container:has(#state-1:checked) { --index: 0; --color: #e91e63; }
.container:has(#state-2:checked) { --index: 1; --color: #2196f3; }
.container:has(#state-3:checked) { --index: 2; --color: #4caf50; }

.indicator {
  background-color: var(--color);
  transform: translateX(calc(var(--index) * 100%));
}

calc関数による動的なスタイル適用

変数を数値として扱うことで、calc() 関数を用いた高度なレイアウト計算が可能になる。例えば、スライダーの移動距離や、要素の不透明度、あるいは hsl() 関数を使った色の変化などを、状態のインデックス番号から動的に算出できる。これは、まるでJavaScriptで計算しているかのような柔軟性をCSSにもたらす。

数値を変化させるシミュレーション
1
2
3

※状態変数 –index の値によってゲージの幅や色を計算

現在のアクティブな状態(変数: 1)  待機中の状態(変数: 2, 3)

このデモは、内部的な変数値の変化がどのように視覚的なゲージやインジケーターに反映されるかを示している。CSSの計算機能を使うことで、滑らかなアニメーションを伴う状態遷移が実現する。

実用性とアクセシビリティの考慮点

実用性とアクセシビリティの考慮点

CSSステートマシンは非常に強力だが、実務で導入する際にはアクセシビリティへの配慮が欠かせない。単に「動く」だけでなく、すべてのユーザーが利用できる形でなければならない。

フォームコントロールとしての特性を活かす

ラジオボタンは本来、フォームの入力要素だ。そのため、キーボード操作(Tabキーでの移動や矢印キーでの選択)に標準で対応している。この特性を壊さないようにスタイリングすることが重要だ。display: none を使ってしまうとフォーカスが当たらなくなるため、視覚的に隠しつつもスクリーンリーダーやキーボードからは認識できる状態を維持する必要がある。

視覚的な変化とセマンティクスのバランス

CSSステートマシンが適しているのは、あくまで「視覚的なバリエーション」や「ローカルなUI操作」だ。データの保存が必要なフォーム送信や、複雑なバリデーションが絡む場合は、おとなしくJavaScriptを使用すべきだ。Kinstaの著者Carlo Daniele氏も指摘するように、CSSはプレゼンテーション層に責任を持ち、アプリケーションのロジックはスクリプト層が持つという役割分担を忘れてはならない。

この記事のポイント

  • ラジオボタンの「1つだけ選択できる」特性を利用して、3つ以上のUI状態をCSSで管理できる
  • :has() や隣接兄弟結合子を駆使することで、進む・戻る・循環といった複雑なフローを制御可能だ
  • カスタムプロパティと calc() を組み合わせれば、状態に応じた動的なレイアウト計算がCSSのみで行える
  • アクセシビリティを損なわないよう、appearance: none を活用し、キーボード操作性を維持することが重要だ
  • 純粋な表示上の状態管理にはCSSを使い、ビジネスロジックにはJavaScriptを使うという適切な使い分けが求められる