テーブル(Table)— 全選択チェックボックス
このコンポーネントについて
一覧テーブルのヘッダーにある1つのチェックボックスで、表示中の全行をまとめて選択・解除できる「全選択チェックボックス(select all checkbox)」の実装例です。一覧から複数行を選んで一括削除・一括ステータス変更する業務テーブルで最も頻出するパターンで、ここではタスク一覧を題材にしています。一部の行だけを選んでいるときはヘッダーを中間状態(indeterminate)で表示し、「いま何件選んでいるか」をツールバーのカウンターで示します。このUIで壊れやすいのは選択状態の管理で、本事例は選択をDOMの checked ではなくデータ側(Set に行IDを保持)で持ち、ヘッダーの状態更新を1つの関数に集約する正しい設計を、動くデモで確認できます。
- ヘッダーの全選択・全解除 — ヘッダーのチェックボックスで表示中の全行を一括ON/OFFする。行チェックの結果でヘッダー側も自動追従する
- indeterminate(中間状態)表示 — 全件でも0件でもない「一部だけ選択」のとき、ヘッダーを中間状態で表示する。
el.indeterminate = trueというJSプロパティでのみ設定できる(HTML属性では指定できない) - 選択件数カウンター — 「N件選択中」をツールバーにリアルタイム表示する。0件のときは「選択を解除」ボタンを
disabledにする - 選択状態はデータ(Set)で保持 — 選択中の行IDを
Setで管理し、チェック状態・ハイライト・ヘッダー状態・カウンターをすべてこの1つの真実から導出する - 選択行のハイライト — チェックした行の背景色を変え、選択中であることを視覚的にフィードバックする
- createElement による安全な行生成 — 行の描画は
createElement+textContentで行い、innerHTMLに値を結合しない(XSS対策)
実装のポイント・注意点
このUIの肝は「選択状態をどこに持つか」です。各行の <input type="checkbox"> の checked をそのまま状態の正にすると、ページングや絞り込みで行が入れ替わったときに選択が壊れます(表示されていない行の選択が失われる、別の行に付いてしまう)。そこで選択中の行IDを Set に持ち、チェックの見た目・行ハイライト・ヘッダーの状態・件数カウンターは、すべてこの Set から導き出します。DOMは「状態の表示先」であって「状態の保管場所」ではない、と切り分けるのがポイントです。
ヘッダーの中間状態は el.indeterminate = true というJavaScriptのプロパティでのみ設定できます。HTMLに <input type="checkbox" indeterminate> と書いても効きません(indeterminate はコンテンツ属性ではなくIDL属性のため)。また checked と indeterminate は独立した値なので、中間状態にしても checked は別に保持されます。ヘッダーの checked / indeterminate / カウンター / 「選択を解除」ボタンの disabled は、選択が変わるたびに1つの関数(updateSelectAllState())にまとめて更新すると、同期漏れが起きません。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
| ID | タスク名 | 担当者 | 期限 | 状態 |
|---|
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>テーブルの全選択チェックボックス サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="dt6-wrap">
<!-- ツールバー(選択件数 + 一括操作の起点) -->
<div class="dt6-toolbar">
<span class="dt6-count" id="dt6-count" aria-live="polite">0件選択中</span>
<button class="dt6-clear-btn" id="dt6-clear" disabled>選択を解除</button>
</div>
<div class="dt6-table-scroll">
<table class="dt6-table">
<thead>
<tr>
<th class="dt6-check-col">
<input type="checkbox" id="dt6-select-all" aria-label="すべて選択">
</th>
<th class="dt6-id-col">ID</th>
<th>タスク名</th>
<th>担当者</th>
<th>期限</th>
<th>状態</th>
</tr>
</thead>
<tbody id="dt6-tbody"><!-- 行はJSで動的生成 --></tbody>
</table>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
:root {
--dt6-primary: #2B7FE8;
--dt6-border: #E2E8F0;
--dt6-text: #1A2332;
--dt6-text-sub: #5A6A7A;
--dt6-head-bg: #F4F7FF;
--dt6-selected-bg: #EBF2FF;
}
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
background: #F0F2F5;
color: var(--dt6-text);
}
.dt6-wrap {
width: 100%;
max-width: 720px;
margin: 0 auto;
}
/* ===== ツールバー ===== */
.dt6-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.dt6-count {
font-size: 13px;
font-weight: 600;
color: var(--dt6-text-sub);
}
.dt6-clear-btn {
padding: 6px 14px;
font-size: 13px;
color: var(--dt6-primary);
background: #fff;
border: 1.5px solid #C7D9F5;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s, border-color 0.15s;
}
.dt6-clear-btn:hover:not(:disabled) {
background: #F0F6FF;
border-color: var(--dt6-primary);
}
.dt6-clear-btn:disabled {
color: #A9B4C2;
border-color: #E2E8F0;
background: #F7F9FC;
cursor: not-allowed;
}
/* ===== テーブル ===== */
.dt6-table-scroll { overflow-x: auto; }
.dt6-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
color: var(--dt6-text);
background: #fff;
}
.dt6-table th,
.dt6-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--dt6-border);
white-space: nowrap;
}
.dt6-table thead th {
background: var(--dt6-head-bg);
font-weight: 600;
color: var(--dt6-text-sub);
}
.dt6-check-col {
width: 44px;
text-align: center;
}
.dt6-id-col { width: 56px; }
.dt6-table input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--dt6-primary);
}
/* ホバー → 選択の順で定義し、選択ハイライトを優先させる */
.dt6-table tbody tr:hover td {
background: #F4F7FF;
transition: background 0.1s;
}
.dt6-table tbody tr.is-selected td {
background: var(--dt6-selected-bg);
}
/* ===== 状態バッジ ===== */
.dt6-status {
display: inline-block;
padding: 2px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 600;
}
.dt6-status[data-status="未着手"] { background: #F3F4F6; color: #6B7280; }
.dt6-status[data-status="対応中"] { background: #DBEAFE; color: #1E40AF; }
.dt6-status[data-status="完了"] { background: #D1FAE5; color: #065F46; }
@media (max-width: 600px) {
.dt6-table th,
.dt6-table td { padding: 8px 12px; }
}
// タスク一覧(行データはJavaScriptの配列で保持する)
const tasks = [
{ id: 1, name: '見積書テンプレートの改訂', assignee: '田中 一郎', due: '2026-07-15', status: '対応中' },
{ id: 2, name: '請求データの月次集計', assignee: '鈴木 花子', due: '2026-07-10', status: '未着手' },
{ id: 3, name: '顧客マスタの重複チェック', assignee: '佐藤 次郎', due: '2026-07-08', status: '完了' },
{ id: 4, name: 'リリースノートの作成', assignee: '山田 三枝', due: '2026-07-20', status: '未着手' },
{ id: 5, name: '問い合わせの一次対応', assignee: '田中 一郎', due: '2026-07-09', status: '対応中' },
{ id: 6, name: 'サーバー証明書の更新', assignee: '高橋 健', due: '2026-07-12', status: '未着手' },
{ id: 7, name: '議事録の共有', assignee: '鈴木 花子', due: '2026-07-07', status: '完了' },
{ id: 8, name: '予算申請の承認依頼', assignee: '伊藤 美咲', due: '2026-07-18', status: '対応中' }
];
// 選択中の行IDを Set で保持する(DOMのcheckedを状態の正としない)
const selected = new Set();
const tbody = document.getElementById('dt6-tbody');
const selectAll = document.getElementById('dt6-select-all');
const countEl = document.getElementById('dt6-count');
const clearBtn = document.getElementById('dt6-clear');
// テキストセルを作る(textContent で安全に表示)
function makeCell(value, className) {
const td = document.createElement('td');
if (className) td.className = className;
td.textContent = value;
return td;
}
// 行を生成する(createElement で組み立て、innerHTML は使わない)
function renderRows() {
tbody.textContent = '';
tasks.forEach(function (task) {
const tr = document.createElement('tr');
tr.dataset.id = task.id;
// チェックボックス列
const tdCheck = document.createElement('td');
tdCheck.className = 'dt6-check-col';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'dt6-row-check';
cb.dataset.id = task.id;
cb.setAttribute('aria-label', task.name + ' を選択');
cb.addEventListener('change', onRowChange);
tdCheck.appendChild(cb);
tr.appendChild(tdCheck);
// データ列
tr.appendChild(makeCell(task.id, 'dt6-id-col'));
tr.appendChild(makeCell(task.name));
tr.appendChild(makeCell(task.assignee));
tr.appendChild(makeCell(task.due));
// 状態バッジ
const tdStatus = document.createElement('td');
const badge = document.createElement('span');
badge.className = 'dt6-status';
badge.dataset.status = task.status;
badge.textContent = task.status;
tdStatus.appendChild(badge);
tr.appendChild(tdStatus);
tbody.appendChild(tr);
});
}
// 個別チェックの変更 → Set を更新して各表示を再導出
function onRowChange(e) {
const id = Number(e.target.dataset.id);
if (e.target.checked) {
selected.add(id);
} else {
selected.delete(id);
}
syncHighlight();
updateSelectAllState();
}
// ヘッダーの全選択 → 全ID追加 or 全クリア
function onSelectAll() {
if (selectAll.checked) {
tasks.forEach(function (t) { selected.add(t.id); });
} else {
selected.clear();
}
syncChecks();
syncHighlight();
updateSelectAllState();
}
// Set をもとに各行の checked を再同期する
function syncChecks() {
tbody.querySelectorAll('.dt6-row-check').forEach(function (cb) {
cb.checked = selected.has(Number(cb.dataset.id));
});
}
// Set をもとに行ハイライトを再同期する
function syncHighlight() {
tbody.querySelectorAll('tr').forEach(function (tr) {
tr.classList.toggle('is-selected', selected.has(Number(tr.dataset.id)));
});
}
// ヘッダー状態・カウンター・ボタンを1関数で更新する(状態更新の集約)
function updateSelectAllState() {
const size = selected.size;
const total = tasks.length;
if (size === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (size === total) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
// 一部だけ選択 → 中間状態(JSプロパティでのみ設定できる)
selectAll.checked = false;
selectAll.indeterminate = true;
}
countEl.textContent = size + '件選択中';
clearBtn.disabled = (size === 0);
}
// 全選択を解除して初期状態へ戻す
function clearSelection() {
selected.clear();
syncChecks();
syncHighlight();
updateSelectAllState();
}
selectAll.addEventListener('change', onSelectAll);
clearBtn.addEventListener('click', clearSelection);
renderRows();
updateSelectAllState();
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や列数変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# テーブルの全選択チェックボックス 作成依頼
## 概要
一覧テーブルのヘッダーにある1つのチェックボックスで表示中の全行をまとめて選択・解除でき、一部だけ選んでいるときはヘッダーを中間状態(indeterminate)で表示する「全選択チェックボックス」を実装してください。題材はタスク一覧です。
## 要件
- 列は「選択(チェックボックス)/ID/タスク名/担当者/期限/状態」。状態は未着手・対応中・完了の色付きバッジで表示する
- 行データはJavaScriptの配列で持ち、行はDOMで動的生成する
- ヘッダーのチェックボックスで表示中の全行を一括ON/OFFする
- 個別チェックの結果に応じてヘッダーの状態を自動更新する。0件=未チェック、全件=チェック、一部=indeterminate(中間状態)にする
- ツールバーに「N件選択中」のカウンターを表示し、選択0件のときは「選択を解除」ボタンをdisabledにする
- チェックした行は背景をハイライトする
- 「選択を解除」ボタンで全解除の初期状態に戻す
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
選択状態はDOMのcheckedではなく、選択中の行IDを保持するSetで管理してください。チェック状態・行ハイライト・ヘッダーの状態・カウンターは、すべてこのSetから導出します。
ヘッダーの中間状態は el.indeterminate = true というJavaScriptのプロパティで設定します(HTML属性では指定できません)。
ヘッダーの状態更新(checked / indeterminate / カウンター / ボタンのdisabled)は1つの関数にまとめ、選択が変わるたびに呼び出してください。
行の描画は createElement と textContent で行い、innerHTML に値を結合しないでください。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。