
CSSだけで多階層の状態を管理するラジオボタン・ステートマシンの実装手法
Web制作における状態管理は、多くの場合JavaScriptの役割だと考えられている。しかし、純粋に視覚的なUIの変化であれば、CSSだけで完結させるアプローチが非常に有効な場合がある。
パネルの開閉やアイコンの形態変化、カードの反転といった「表示上の状態」をCSSで管理することで、JavaScriptのオーバーヘッドを削減し、プレゼンテーション層に近い場所でロジックを保持できる。この記事では、従来のチェックボックスハックを進化させた「ラジオボタン・ステートマシン」という手法について詳しく解説する。
この手法をマスターすると、複雑な多段階のUI遷移もHTMLと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タグを最小限に抑えたまま、インタラクティブな要素が完成する。
状態切り替えデモ(簡易版)
このデモはラジオボタンの排他的な性質を利用した状態遷移を視覚化したものだ。実際の実装では、クリックするたびに :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にもたらす。
※状態変数 –index の値によってゲージの幅や色を計算
このデモは、内部的な変数値の変化がどのように視覚的なゲージやインジケーターに反映されるかを示している。CSSの計算機能を使うことで、滑らかなアニメーションを伴う状態遷移が実現する。
実用性とアクセシビリティの考慮点

CSSステートマシンは非常に強力だが、実務で導入する際にはアクセシビリティへの配慮が欠かせない。単に「動く」だけでなく、すべてのユーザーが利用できる形でなければならない。
フォームコントロールとしての特性を活かす
ラジオボタンは本来、フォームの入力要素だ。そのため、キーボード操作(Tabキーでの移動や矢印キーでの選択)に標準で対応している。この特性を壊さないようにスタイリングすることが重要だ。display: none を使ってしまうとフォーカスが当たらなくなるため、視覚的に隠しつつもスクリーンリーダーやキーボードからは認識できる状態を維持する必要がある。
視覚的な変化とセマンティクスのバランス
CSSステートマシンが適しているのは、あくまで「視覚的なバリエーション」や「ローカルなUI操作」だ。データの保存が必要なフォーム送信や、複雑なバリデーションが絡む場合は、おとなしくJavaScriptを使用すべきだ。Kinstaの著者Carlo Daniele氏も指摘するように、CSSはプレゼンテーション層に責任を持ち、アプリケーションのロジックはスクリプト層が持つという役割分担を忘れてはならない。
この記事のポイント
- ラジオボタンの「1つだけ選択できる」特性を利用して、3つ以上のUI状態をCSSで管理できる
:has()や隣接兄弟結合子を駆使することで、進む・戻る・循環といった複雑なフローを制御可能だ- カスタムプロパティと
calc()を組み合わせれば、状態に応じた動的なレイアウト計算がCSSのみで行える - アクセシビリティを損なわないよう、
appearance: noneを活用し、キーボード操作性を維持することが重要だ - 純粋な表示上の状態管理にはCSSを使い、ビジネスロジックにはJavaScriptを使うという適切な使い分けが求められる

・ 複数業界における17年間のデジタルビジネス開発経験
・ ウェブサイト開発のためのHTML、PHP、CSS、Java等の実用的知識
・ 15ヶ国語対応の多言語SaaSの開発経験
・ 17年間にも及ぶ、Eコマース長期運営経験
・ 幅広い業界でのSEO最適化の豊富な経験
