【テクニカル・上級編】 indeterminate疑似クラス – CSS実践ガイド

誰も語らない `:indeterminate` の深層:中間状態という「曖昧さ」をCSSでどう制圧するか

フロントエンドのアーキテクチャ設計において、状態管理は常に頭の痛い問題だ。特に「ONでもOFFでもない、その中間の状態」を扱うとき、私たちはしばしばJavaScriptの海に溺れ、DOMの不整合という名のモンスターと戦うことになる。

「すべて選択」のマスターチェックボックスを実装したことがあるなら、子要素が一部だけ選択されたときに生じる「あの何とも言えない中途半端な状態」に直面したはずだ。HTMLの仕様上、`` には `checked` と `unchecked` の二値しか存在しない。しかし、UXの現場はそんなに甘くない。

ここで登場するのが、本日スポットを当てる `:indeterminate` 疑似クラス だ。

今回は、この隠れた実力者を単なる「見た目を変える便利なセレクタ」としてではなく、モダンブラウザのレンダリングエンジン、メモリ効率、そして非同期処理が絡む複雑な状態競合のコンテキストから徹底的に解剖していく。ギークな視点から、この曖昧な状態を完全にコードで制御するためのアーキテクチャ論を語ろう。

—

1. `:indeterminate` の本質:なぜCSSセレクタでありながらJSプロパティなのか?

まず前提として、多くの開発者が勘違いしている事実を指摘しておこう。`:indeterminate` は、他の `:hover` や `:focus` とは根本的に毛色が違う。これらはユーザーのインタラクションによってブラウザが勝手に状態をトグルしてくれるが、`:indeterminate` はCSSだけで完結しないケースがほとんどであるという点だ。

HTMLのマークアップにおいて、チェックボックスに `indeterminate` という属性(Attribute)は存在しない。


正確には、DOMプロパティ(Property)としての `HTMLInputElement.indeterminate = true` がアサインされた瞬間に、ブラウザの内部ステートマシンが切り替わり、このCSS疑似クラスが発火する仕組みになっている。

つまり、この疑似クラスを使いこなすということは、「JavaScriptによるステートの変更」と「CSSによるレイアウトの同期」のパイプラインを極限まで美しく設計することに他ならない。

レンダリングエンジン内部の挙動とパフォーマンス

ブラウザ(Blink, Gecko, WebKit)のレンダリングエンジンにおいて、`:indeterminate` が付与された要素は、視覚的に「ハイフン(マイナス記号)」や「塗りつぶされた四角形」を描画する必要がある。これは通常のチェックマーク(`checked`)とは異なるペイント処理を伴う。

ここで重要なのは、無駄なレイアウトスラッシング(Layout Thrashing)を引き起こさないことだ。頻繁にDOMを書き換えるSPA(Single Page Application)において、親・子チェックボックスの関係性を雑にJSで監視すると、メインスレッドがブロッキングされ、スクロールのコマ落ちは免れない。

後述するアプローチで、このコストを最小限に抑えるアーキテクチャを見ていこう。

—

2. 実践的アーキテクチャ:堅牢な「マスター・スレーブ」パターン

「すべて選択」のUIを例に、バグの温床になりやすいコードと、それを美しく解決する実用的なコードを比較する。

ありがちなアンチパターン(JSでクラスを付け替える愚行)

よくあるのが、JS側で子要素の状態を計算し、親要素に `.is-half-checked` のような自作クラスを付与する手法だ。これではCSSとDOMの責務が曖昧になり、ReactやVueなどの宣言的UIフレームワークと激しく衝突する。

正解:DOMプロパティとCSS疑似クラスの完全同期

フレームワークのRefや副作用フック(`useEffect` / `onMounted` 等)を使い、DOMの `indeterminate` プロパティを直接制御する。CSS側は以下のように純粋に `:indeterminate` セレクタに責任を持たせる。

/ ==========================================
Checkbox indeterminate Architecture CSS
================================———- /

.master-checkbox {
/ カスタムプロパティでデザインシステムと統合 /
–checkbox-border-color: #cbd5e1;
–checkbox-active-color: #3b82f6;
–checkbox-indeterminate-bg: #3b82f6;
}

/ 通常のチェックボックスの基本スタイルリセット /
.master-checkbox input[type=”checkbox”] {
appearance: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(–checkbox-border-color);
border-radius: 0.25rem;
outline: none;
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease;
position: relative;
}

/ フォーマットのアクセシビリティ担保 /
.master-checkbox input[type=”checkbox”]:focus-visible {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}

/ 1. チェック状態 (checked) /
.master-checkbox input[type=”checkbox”]:checked {
background-color: var(–checkbox-active-color);
border-color: var(–checkbox-active-color);
/ チェックマークの描画(擬似要素) /
background-image: url(“data:image/svg+xml,%3Csvg xmlns=’http://www.w3.org/2000/svg’ viewBox=’0 0 24 24′ fill=’none’ stroke=’white’ stroke-width=’3′ stroke-linecap=’round’ stroke-linejoin=’round’%3E%3Cpolyline points=’20 6 9 17 4 12’%3E%3C/polyline%3E%3C/svg%3E”);
background-size: 80%;
background-position: center;
background-repeat: no-repeat;
}

/ 2. 中間状態 (indeterminate) – ここが今回の主役 /
.master-checkbox input[type=”checkbox”]:indeterminate {
background-color: var(–checkbox-indeterminate-bg);
border-color: var(–checkbox-active-color);
/ マイナス(ハイフン)アイコンの描画 /
background-image: url(“data:image/svg+xml,%3Csvg xmlns=’http://www.w3.org/2000/svg’ viewBox=’0 0 24 24′ fill=’none’ stroke=’white’ stroke-width=’3′ stroke-linecap=’round’ stroke-linejoin=’round’%3E%3Cline x1=’5′ y1=’12’ x2=’19’ y2=’12’%3E%3C/line%3E%3C/svg%3E”);
background-size: 80%;
background-position: center;
background-repeat: no-repeat;
}

/ 3. 競合・排他制御:indeterminate と checked が同時に存在する場合の安全網 /
/ ブラウザ仕様上、indeterminate が true の時、checked がどうあれ中間状態が優先されるが、
CSS側でも明確に意図を定義しておくことで予期せぬスタイルの崩壊を防ぐ /
.master-checkbox input[type=”checkbox”]:indeterminate:checked {
/ 必要であれば特殊なスタイルをあてるが、通常はindeterminateのスタイルを継承で十分 /
}

—

3. 非同期の競合とメモリ効率:実務で踏みがちな地雷

高度なWebアプリケーション(例えば、数千件のレコードを持つ管理画面のデータグリッド)で `:indeterminate` を扱う際、エンジニアが陥りがちな罠がある。

地雷1:非同期データ取得時の「状態のデッドロック」

子要素のチェック状態がAPI経由の非同期フェッチ(ページネーションや遅延ロード)によって動的に変化する場合、マスター側の `indeterminate` 判定ロジックが古いDOMを参照し、バグる現象が起きる。

回避策:
状態の真実のソース(Single Source of Truth)をDOMではなくJavaScriptのメモリ上(StoreやState)に置き、ステートが更新された「次のティック」で確実にDOMプロパティを同期させること。

// バニラJSまたはフレームワークのロジックの概念実証
function updateMasterCheckbox(masterEl, childrenStates) {
const allChecked = childrenStates.every(Boolean);
const noneChecked = childrenStates.every(val => !val);

// DOMプロパティへ直接アサイン(属性ではない点に注意)
if (!allChecked && !noneChecked) {
masterEl.indeterminate = true;
masterEl.checked = false; // 状態の矛盾を防ぐため明示的にfalseへ
} else {
masterEl.indeterminate = false;
masterEl.checked = allChecked;
}
}

地雷2:不要なリスナーによるメモリリーク

大量のチェックボックスに対して個別に `change` イベントリスナーをアタッチすると、コンポーネントの破棄時にクリーンアップを忘れた場合、GC(ガベージコレクション)が働かず、メモリリークの温床になる。

アーキテクチャの知見:
イベント委譲(Event Delegation)を使い、親コンテナでイベントを一括キャッチする構造にせよ。これにより、DOMノードが増減してもメモリフットプリントを一定に保つことができる。

—

4. ラジオボタンにおける `:indeterminate` の深すぎる闇

最後に、ギークなら知っておくべきトリビアを一つ。
`:indeterminate` はチェックボックスだけでなく、実は`` でも仕様上定義されている。

すべてのラジオグループの中で、「どのラジオボタンも選択されていない(`checked` が一つもない)初期状態」のとき、グループ内のすべてのラジオボタンが一時的に `:indeterminate` 状態になる…と言いたいところなのだが、実際のブラウザ実装ではほとんどサポートされていない、あるいは挙動がバラバラである。

ラジオボタンにおいてユーザーが一度どれかを選択すると、そのグループから「無選択(indeterminate)」の状態に戻すことは、通常のHTMLの仕様上不可能になる(リセットボタン等を使わない限り)。そのため、ラジオボタンにおける `:indeterminate` は、実務の現場では「地雷原」として封印するのが賢明だ。我々はチェックボックスのマスター・スレーブ制御、あるいはカスタムセレクトのアクセシビリティ向上に集中すべきである。

—

総括:曖昧さをコードで統御する美学

`:indeterminate` 疑似クラスは、単なる見た目の装飾ツールではない。それは「二値(ON/OFF)の世界に存在する例外的な三値目を、宣言的かつエレガントに表現するための架け橋」である。

JavaScriptの泥臭いDOM操作と、CSSの洗練されたスタイル定義。この2つが正確に噛み合ったとき、アプリケーションのUIは一段上の「堅牢性」を獲得する。

公式マニュアルの表面的な解説をなぞるだけでは到達できないレイヤーで、コードの隅々にまで意図を宿すこと。それこそが、真のフロントエンド・アーキテクタの仕事なのだ。

コメント

タイトルとURLをコピーしました