フロントエンドの荒波にもまれながら、日々CSSと格闘している中級エンジニアの皆さん、お疲れ様です。チーフアーキテクトの私だ。
今回は、CSSの数ある疑似クラスの中でも、実務で意外と見落とされがちでありながら、「おっ、こいつ分かってるな」とレビューで一目置かれる隠し味的な存在である `:indeterminate` 疑似クラスについて徹底的に掘り下げていこうと思う。
「チェックボックスの「チェック済み(checked)」と「未チェック(unchecked)」以外に状態なんてあるの?」と思ったそこの君。甘い。実務の現場では、いわゆる「全選択・部分選択(三毛猫状態)」を表現しなければならないシーンが山のようにやってくる。そんなとき、JavaScriptで無理やりクラスを付け外しして消耗していないか?
ブラウザのネイティブな仕様を正しく理解し、CSSだけでスマートに状態をコントロールする方法をここでマスターして帰ってくれ。
—
1. `:indeterminate` 疑似クラスとは何か?(標準仕様とブラウザの裏側)
まずは基本の定義からいこう。`:indeterminate` は、「値が明確に決定されていない、中間的な状態にあるフォーム要素」にマッチする疑似クラスだ。
主に以下の要素に対して適用される。
1. `type=”checkbox”` の入力要素
2. `type=”radio”` のラジオボタングループ
3. `
特に実務で圧倒的に出番が多いのは 「チェックボックスの中間状態」 だ。
例えば、リストの一括選択機能で、5個ある子チェックボックスのうち「2個だけ」にチェックが入っている親チェックボックスを想像してほしい。全選択でも全解除でもない、あの「マイナス記号(➖)」が表示されている状態こそが、まさに `indeterminate` だ。
⚠️ ここが重要:CSSだけでは「ON」にできない
初心者がやりがちな勘違いとして、「CSSで `:indeterminate` を書けば勝手に中間状態になるんでしょ?」というものがある。
違う。`:indeterminate` はあくまで「状態を検知してスタイルを当てる」ための疑似クラスであり、状態そのものをCSSだけで「ON」にすることは原則できない。 (※ラジオボタンのグループ選択外を除く)
この中間状態(indeterminate)を有効にするには、JavaScriptからDOMプロパティとして明示的に指示を与えてやる必要がある。
// JavaScriptからDOMプロパティでindeterminateをtrueにする
const checkbox = document.querySelector(‘#parent-checkbox’);
checkbox.indeterminate = true;
HTML属性として `indeterminate` と書いても機能しない(HTMLにはそんな属性は存在しない)という点に注意してほしい。これはブラウザの「DOMプロパティ」なのだ。ここを勘違いしていると、「何度CSSを書いてもスタイルが当たらない!」と深夜のオフィスで絶望することになるので、しっかりと頭に叩き込んでおいてくれ。
—
2. なぜJSのクラス切り替えより `:indeterminate` が優れているのか?
実務でよくあるアンチパターンがこれだ。
「よし、子要素が一部選択されたら、親チェックボックスに `.is-indeterminate` ってクラスをJSで付与してスタイルを当てよう」
ちょっと待て。それ、本当にメンテナンスしやすいか?
CSSのアーキテクチャの観点から言えば、「状態(State)」は可能な限りブラウザのネイティブな状態(Pseudo-class)に委ねるべきだ。DOMのクラス肥大化を防げるし、何より「CSSとJSの関心の分離」が美しく決まる。
ブラウザのエンジン(BlinkやWebKitなど)は、内部のフォームコントロールの状態管理とCSSセレクタを非常に高速に結びつけている。ネイティブの疑似クラスを使うことで、レンダリングパイプラインにおいても無駄なクラス再計算を避けることができ、パフォーマンス面でも有利に働くのだ。
—
3. 【コピペで使える】実務直結!美しくモダンな全選択UIの実装例
百聞は一見に如かず。ここでは、実務の管理画面などで頻出する「親子チェックボックスの連動UI」のサンプルコードを提示しよう。HTML、CSS、最小限のJSを組み合わせた、そのままプロダクションコードに組み込めるクオリティのものを用意した。
HTML
CSS
ここでは、ネイティブのチェックボックスを隠し、カスタムデザインしたスパン要素に対して `:checked` と `:indeterminate` のスタイルを適用している。
/ ——————————–ランスタイル(リセットとベース) ——————————– /
.checkbox-group {
font-family: sans-serif;
color: #333;
}
.sub-checkboxes {
list-style: none;
padding-left: 24px;
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
/ ラベル全体のレイアウト /
.checkbox-label {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
/ ネイティブのチェックボックスはアクセシビリティを保ったまま視覚的に隠す /
.checkbox-label input[type=”checkbox”] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/ カスタムチェックボックスの外観 /
.checkbox-custom {
position: relative;
width: 18px;
height: 18px;
border: 2px solid #cbd5e1;
border-radius: 4px;
background-color: #fff;
transition: background-color 0.2s, border-color 0.2s;
}
/ ホバー時のインタラクション /
.checkbox-label:hover .checkbox-custom {
border-color: #94a3b8;
}
/ ——————————–
1. チェック状態(:checked)のスタイル
——————————– /
.checkbox-label input[type=”checkbox”]:checked + .checkbox-custom {
background-color: #2563eb;
border-color: #2563eb;
}
/ チェックマーク(✔)の描画 /
.checkbox-label input[type=”checkbox”]:checked + .checkbox-custom::after {
content: “”;
position: absolute;
left: 5px;
top: 1px;
width: 4px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/ ——————————–
2. 中間状態(:indeterminate)のスタイル
——————————– /
.checkbox-label input[type=”checkbox”]:indeterminate + .checkbox-custom {
background-color: #2563eb;
border-color: #2563eb;
}
/ 中間状態のアイコン(➖ マイナス記号)の描画 /
.checkbox-label input[type=”checkbox”]:indeterminate + .checkbox-custom::after {
content: “”;
position: absolute;
left: 3px;
top: 6px;
width: 10px;
height: 2px;
background-color: white;
/ rotateや複雑な変形は不要。シンプルに横棒を作る /
}
/ フォーマットのアクセシビリティ(キーボード操作時にフォーカスリングを明示) /
.checkbox-label input[type=”checkbox”]:focus-visible + .checkbox-custom {
outline: 2px solid #93c5fd;
outline-offset: 2px;
}
JavaScript(状態の制御ロジック)
ついでに、このCSSを完璧に駆動させるための最小限のJSも添えておこう。実務でそのまま使えるロジックだ。
document.addEventListener(‘DOMContentLoaded’, () => {
const selectAllCheckbox = document.querySelector(‘#select-all’);
const subCheckboxes = document.querySelectorAll(‘.sub-item’);
// 子要素の状態を監視して、親の状態を更新する関数
const updateParentState = () => {
const checkedCount = Array.from(subCheckboxes).filter(cb => cb.checked).length;
const totalCount = subCheckboxes.length;
if (checkedCount === 0) {
// 全て未チェック
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === totalCount) {
// 全てチェック済み
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
//一部だけチェックされている(=中間状態)
selectAllCheckbox.checked = false; // つじつま合わせとしてfalseにするのが安全
selectAllCheckbox.indeterminate = true;
}
};
// 子要素が変更されたときのイベント
subCheckboxes.forEach(cb => {
cb.addEventListener(‘change’, updateParentState);
});
// 親要素がクリックされたときの挙動(全選択 / 全解除のトグル)
selectAllCheckbox.addEventListener(‘change’, (e) => {
// indeterminate状態のときにクリックされたら、次は「全選択」に倒すのが一般的
const isTargetChecked = e.target.checked;
subCheckboxes.forEach(cb => {
cb.checked = isTargetChecked;
});
// 親自身の indeterminate は手動で解除する
selectAllCheckbox.indeterminate = false;
});
});
このコードを動かしてみればわかるが、子要素のチェック状況に応じて、親のチェックボックスが「未選択」「中間(マイナス)」「全選択(チェック)」に滑らかに切り替わる。すべてCSSの `:indeterminate` が裏側で美しくレンダリングを支えているおかげだ。
—
4. チーフアーキテクトからの実務アドバイス
最後に、現場でこの技術を使う上での「プロの知見」をいくつか共有しておこう。
1. JSとの組み合わせの順序に注意する
JSで `checkbox.checked = true` などを実行すると、ブラウザの仕様上、自動的に `indeterminate` プロパティが `false` にリセットされる。そのため、コードを書く際は「先に `checked` やその他の状態をいじり、最後に必要に応じて `indeterminate = true` を代入する」という順番を守る必要がある。この仕様を知らないと、バグにハマって半日溶かすことになるので注意してほしい。
2. デザインシステムやコンポーネントライブラリへの組み込み
もし君たちのチームが React, Vue, Svelte などのモダンなフレームワークを使っている場合でも、この原則は変わらない。親コンポーネントのロジック内で `ref` を経由して DOM の `indeterminate` プロパティを書き換えるアプローチをとることで、CSS側のセレクタは純粋に `:indeterminate` に頼ることができる。CSSに無駄なユーティリティクラス(`.is-half-checked` など)を乱立させずに済むため、デザインシステムの保守性が劇的に向上する。
—
さあ、理屈と実践の準備は整った。
次に「全選択・部分選択」のUIを実装する機会に出くわしたとき、クラスの付け外しで泥臭くコードを書くのはもう終わりだ。ブラウザのネイティブな仕様である `:indeterminate` をスマートに使いこなし、ワンランク上の洗練されたフロントエンドを構築してくれ。
君たちの健闘を祈る!

コメント