【JavaScript】「全て選択」チェックボックスの実装ガイド|indeterminate状態・グループ別・テーブル行選択まで完全解説

「全て選択」チェックボックスは、メールクライアントやデータ管理画面でよく見かけるUIです。一見シンプルに見えますが、個別チェックとの連動一部だけチェックされたときの不確定状態まで正しく実装するには、いくつか押さえておくべきポイントがあります。

この記事では、基本実装から indeterminate(不確定)状態、テーブル行選択、グループ別「全て選択」まで実践的なパターンを体系的に解説します。

スポンサーリンク

基本実装:全て選択と個別チェックの連動

最初に「全て選択」と個別チェックボックスが双方向に連動する基本実装を作ります。

HTML
<div class="checkbox-group">
  <label class="select-all-label">
    <input type="checkbox" id="selectAll"> 全て選択
  </label>
  <hr>
  <label><input type="checkbox" class="item-check" value="apple">  りんご</label>
  <label><input type="checkbox" class="item-check" value="banana"> バナナ</label>
  <label><input type="checkbox" class="item-check" value="orange"> オレンジ</label>
  <label><input type="checkbox" class="item-check" value="grape">  ぶどう</label>
</div>
全て選択の基本実装
const selectAll  = document.getElementById('selectAll');
const itemChecks = document.querySelectorAll('.item-check');

// ① 「全て選択」チェック → 個別チェックを全て同期
selectAll.addEventListener('change', () => {
  itemChecks.forEach((cb) => {
    cb.checked = selectAll.checked;
  });
});

// ② 個別チェックの変更 → 「全て選択」の状態を更新
itemChecks.forEach((cb) => {
  cb.addEventListener('change', updateSelectAll);
});

function updateSelectAll() {
  const total   = itemChecks.length;
  const checked = Array.from(itemChecks).filter((cb) => cb.checked).length;

  if (checked === 0) {
    selectAll.checked       = false;
    selectAll.indeterminate = false;
  } else if (checked === total) {
    selectAll.checked       = true;
    selectAll.indeterminate = false;
  } else {
    selectAll.checked       = false;
    selectAll.indeterminate = true; // 一部チェック → 不確定状態
  }
}
indeterminate プロパティとは:indeterminate = true にすると、チェックボックスが「☑(オン)」でも「□(オフ)」でもない、「−」のような不確定状態をUIで表現できます。これは checked プロパティとは独立した CSS/視覚状態であり、HTML属性では設定できません(JavaScriptのみ)。ユーザーが一部だけチェックしていることを直感的に伝えられます。

indeterminate 状態のスタイリング

CSS の :indeterminate 擬似クラスを使うと、不確定状態のチェックボックスを独自にスタイリングできます。

indeterminate スタイル
/* 不確定状態の見た目(ブラウザデフォルトとは別に独自スタイルを追加したい場合) */
input[type="checkbox"]:indeterminate + span,
input[type="checkbox"]:indeterminate ~ .label-text {
  color: #6b7280;
  font-style: italic;
}

/* カスタムチェックボックスで indeterminate を「−」で表示する例 */
.custom-cb {
  appearance: none;
  width: 18px;
  height: 18px;
  border: 2px solid #94a3b8;
  border-radius: 3px;
  position: relative;
  cursor: pointer;
  vertical-align: middle;
}

.custom-cb:checked {
  background: #0284c7;
  border-color: #0284c7;
}

.custom-cb:checked::after {
  content: "";
  position: absolute;
  left: 4px; top: 1px;
  width: 6px; height: 10px;
  border: 2px solid #fff;
  border-top: none; border-left: none;
  transform: rotate(45deg);
}

.custom-cb:indeterminate {
  background: #0284c7;
  border-color: #0284c7;
}

.custom-cb:indeterminate::after {
  content: "";
  position: absolute;
  left: 3px; top: 6px;
  width: 8px; height: 2px;
  background: #fff; /* 横棒「−」 */
}

テーブルの行選択UI

管理画面でよく使われる「ヘッダー行で全選択・各行で個別選択」パターンです。チェックした行をハイライトし、選択件数もリアルタイムで表示します。

HTML(テーブル)
<div class="table-actions">
  <span id="selectedCount">0件選択中</span>
  <button id="deleteSelected" disabled>削除</button>
</div>
<table id="dataTable">
  <thead>
    <tr>
      <th><input type="checkbox" id="tableSelectAll" aria-label="全て選択"></th>
      <th>名前</th>
      <th>メール</th>
      <th>ステータス</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><input type="checkbox" class="row-check" value="1"></td>
      <td>田中 太郎</td><td>tanaka@example.com</td><td>有効</td>
    </tr>
    <tr>
      <td><input type="checkbox" class="row-check" value="2"></td>
      <td>佐藤 花子</td><td>sato@example.com</td><td>有効</td>
    </tr>
    <tr>
      <td><input type="checkbox" class="row-check" value="3"></td>
      <td>鈴木 一郎</td><td>suzuki@example.com</td><td>無効</td>
    </tr>
  </tbody>
</table>
CSS(選択行ハイライト)
tr.selected {
  background: #eff6ff;
}

tr.selected td {
  border-color: #bfdbfe;
}
テーブル行選択の実装
const tableSelectAll = document.getElementById('tableSelectAll');
const rowChecks      = document.querySelectorAll('.row-check');
const countDisplay   = document.getElementById('selectedCount');
const deleteBtn      = document.getElementById('deleteSelected');

// 選択件数の表示更新
function updateCount() {
  const selected = Array.from(rowChecks).filter((cb) => cb.checked);
  countDisplay.textContent = `${selected.length}件選択中`;
  deleteBtn.disabled = selected.length === 0;
}

// 全選択ヘッダーの状態更新
function updateHeader() {
  const total   = rowChecks.length;
  const checked = Array.from(rowChecks).filter((cb) => cb.checked).length;
  tableSelectAll.checked       = checked === total;
  tableSelectAll.indeterminate = checked > 0 && checked < total;
  updateCount();
}

// ヘッダーチェック → 全行に反映
tableSelectAll.addEventListener('change', () => {
  rowChecks.forEach((cb) => {
    cb.checked = tableSelectAll.checked;
    cb.closest('tr').classList.toggle('selected', cb.checked);
  });
  updateCount();
});

// 行チェック → ハイライト + ヘッダー更新
rowChecks.forEach((cb) => {
  cb.addEventListener('change', () => {
    cb.closest('tr').classList.toggle('selected', cb.checked);
    updateHeader();
  });
});

グループ別「全て選択」

カテゴリごとに「全て選択」ボタンを分けるパターンです。data-group 属性で関連付けることで、汎用的に実装できます。

HTML(グループ別)
<fieldset>
  <legend>果物</legend>
  <label><input type="checkbox" class="group-all" data-group="fruit"> 全て選択</label>
  <label><input type="checkbox" class="group-item" data-group="fruit" value="apple">  りんご</label>
  <label><input type="checkbox" class="group-item" data-group="fruit" value="banana"> バナナ</label>
  <label><input type="checkbox" class="group-item" data-group="fruit" value="grape">  ぶどう</label>
</fieldset>
<fieldset>
  <legend>野菜</legend>
  <label><input type="checkbox" class="group-all" data-group="veggie"> 全て選択</label>
  <label><input type="checkbox" class="group-item" data-group="veggie" value="carrot">  にんじん</label>
  <label><input type="checkbox" class="group-item" data-group="veggie" value="spinach"> ほうれん草</label>
  <label><input type="checkbox" class="group-item" data-group="veggie" value="onion">   玉ねぎ</label>
</fieldset>
グループ別「全て選択」の実装
// 全ての「全て選択」チェックボックスに一括でイベントを設定
document.querySelectorAll('.group-all').forEach((allCb) => {
  const group = allCb.dataset.group;
  const items = document.querySelectorAll(`.group-item[data-group='${group}']`);

  // 「全て選択」→ グループ内の個別チェックを同期
  allCb.addEventListener('change', () => {
    items.forEach((cb) => { cb.checked = allCb.checked; });
  });

  // 個別チェック → 「全て選択」の状態を更新
  items.forEach((cb) => {
    cb.addEventListener('change', () => {
      const checkedCount = Array.from(items).filter((c) => c.checked).length;
      allCb.checked       = checkedCount === items.length;
      allCb.indeterminate = checkedCount > 0 && checkedCount < items.length;
    });
  });
});
data-group 属性でグルーピングするメリット:グループの数や名前が増えても JavaScript コードを変更する必要がありません。HTML側に data-group="新しいグループ名" を追加するだけで自動的に対応します。

選択した項目の取得・操作

「送信」ボタンを押したときなど、チェックされた値を取得するパターンです。

選択済み value を配列で取得
// querySelectorAll で checked なものを絞り込む
function getCheckedValues(selector) {
  return Array.from(document.querySelectorAll(selector))
    .filter((cb) => cb.checked)
    .map((cb) => cb.value);
}

// 使い方
const selected = getCheckedValues('.item-check');
console.log(selected); // ['apple', 'orange'] など

// FormData を使う方法(form 要素内のチェックボックスに限る)
const form = document.getElementById('myForm');
const formData = new FormData(form);
const values = formData.getAll('items'); // name 属性が同じ複数チェックボックス
console.log(values); // チェックされた value の配列
「全て解除」ボタンの実装
document.getElementById('clearAll').addEventListener('click', () => {
  document.querySelectorAll('.item-check').forEach((cb) => {
    cb.checked = false;
  });
  // 全て選択チェックボックスもリセット
  selectAll.checked       = false;
  selectAll.indeterminate = false;
});

アクセシビリティ対応

チェックボックスのアクセシビリティは、スクリーンリーダーが状態を正しく読み上げられるかどうかがポイントです。

対応項目 実装方法 効果
ラベルの紐付け <label> で囲む or for 属性 クリック領域拡大 + 読み上げ対応
「全て選択」の役割説明 aria-label="全て選択" 単独のチェックボックスに意味を付与
グループのまとめ <fieldset> + <legend> グループの名前をスクリーンリーダーが読み上げる
indeterminate の読み上げ ブラウザが自動対応(”mixed” と読み上げ) 追加実装不要
アクセシビリティ対応済みHTML例
<fieldset>
  <legend>商品を選択</legend>
  <label>
    <input
      type="checkbox"
      id="selectAllA11y"
      aria-label="商品を全て選択"
    >
    全て選択
  </label>
  <label><input type="checkbox" class="product-check" value="p1"> 商品A</label>
  <label><input type="checkbox" class="product-check" value="p2"> 商品B</label>
  <label><input type="checkbox" class="product-check" value="p3"> 商品C</label>
</fieldset>
indeterminate とスクリーンリーダー:indeterminate = true の状態は、主要ブラウザでは自動的に “mixed”(混在)と読み上げられます。独自の aria-checked="mixed" を追加することでさらに明示的にできますが、通常のチェックボックスには aria-checked を付けると逆に混乱するため、role=”checkbox” を明示的に使う場合のみ aria-checked を使用してください。

よくある質問

Qindeterminate を HTML属性で設定できますか?
Aできません。indeterminate は JavaScript のプロパティとしてのみ設定できます。<input indeterminate> のような HTML属性は存在しません。必ず el.indeterminate = true とJavaScriptで設定してください。
Q動的に追加された行のチェックボックスも「全て選択」と連動させるには?
Aイベント委譲(Event Delegation)を使います。個別チェックボックスへのイベントを親要素(テーブルの tbody など)に設定し、e.target.closest(".row-check") で判定することで、後から追加された要素にも対応できます。
Qページ読み込み時に一部チェック済みの場合、indeterminate を初期化するには?
Aページ読み込み後に updateSelectAll()(上記実装の状態更新関数)を一度呼び出すだけです。現在の checked 状態を読み取って自動的に indeterminate を設定します。
Qチェックボックスの値をサーバーに送信するには?
Aフォーム内に配置して name 属性を同名にすることで、チェックされた value が配列として送信されます。または JavaScript で getCheckedValues() を使って取得し、fetch でJSON送信する方法もあります。
Q「全て選択」チェックボックスのラベルクリックで選択が切り替わらない場合は?
A<label> タグでチェックボックスを囲むか、for 属性と id 属性を紐付けてください。両方の方法でクリック領域を広げてユーザビリティを向上できます。

まとめ

「全て選択」チェックボックスの実装ポイントをまとめます。

  • indeterminate = true で一部チェック時の「不確定状態」を視覚的に表現できる
  • 個別チェックの変更時は every/filter で全体の状態を判定し、checked と indeterminate を同時に更新する
  • data-group 属性を使うとグループ別「全て選択」を汎用的に実装できる
  • 動的追加要素への対応はイベント委譲で解決できる
  • <fieldset> + <legend> でグループをまとめるとアクセシビリティが向上する

チェックボックスと連動してボタンのアクティブ状態を切り替える方法は【JavaScript】チェックボックスと連動したボタンの活性化、classList の操作は【JavaScript】classList の使い方完全ガイドもあわせてご覧ください。