テーブル(Table)— 選択行の一括ステータス変更

データ表示 初級

このコンポーネントについて

テーブルで選択した複数行のステータスを、ツールバーのプルダウンと「適用」ボタンでまとめて変更する「一括ステータス変更(bulk status update)」の実装例です。タスクの進捗を複数まとめて「対応中」から「完了」に切り替える、といった業務テーブルの定番操作を題材にしています。選択状態の管理は全選択チェックボックスの事例と同じ設計(選択中の行IDを Set で保持し、ヘッダー状態の更新を1つの関数に集約する)を踏襲しており、一括削除の選択行の一括削除の事例と並ぶツールバー拡張パターンの1つです。行を選ぶ→変更先を選ぶ→適用する、という一括操作UIの基本的な作法を、実際に触って確認できます。

  • ステータス変更プルダウン+適用ボタン — ツールバーに変更先ステータス(未着手・対応中・完了)を選ぶプルダウンと「適用」ボタンを配置し、選択中の全行のステータスを一括で切り替える
  • 2段階の活性制御 — 「行が1件以上選択されている」かつ「プルダウンで変更先が選ばれている」の両方を満たすまで「適用」ボタンを disabled にする
  • 適用後の自動選択解除 — 適用が完了すると選択・ハイライト・ヘッダー状態をすべて解除する。連続で別のステータスへ誤って再適用する事故を防ぐ設計
  • 選択状態はデータ(Set)で保持全選択チェックボックスの事例と同じく、選択中の行IDを Set で管理し、チェック状態・ハイライト・ヘッダー状態・カウンターをこの1つの真実から導出する
  • ステータスバッジの配色統一 — 未着手(グレー)・対応中(青)・完了(緑)の配色を全選択チェックボックスの事例から踏襲する
  • createElement による安全な行生成 — 行の描画は createElementtextContent で行い、innerHTML に値を結合しない(XSS対策)

実装のポイント・注意点

「適用」ボタンの活性・非活性は、選択件数とプルダウンの選択値という2つの条件を1つの関数(updateApplyButtonState())にまとめて判定しています。片方だけを見て判定するコードを2箇所に分けて書くと、選択を変えたときとプルダウンを変えたときで判定ロジックがズレてしまう事故が起きやすいためです。両方のイベントから同じ関数を呼ぶことで、常に同じ基準でボタンの状態が決まります。

ステータス変更には確認モーダルを設けていません。同じ一括操作でも「削除」は取り消しにくい操作のため選択行の一括削除の事例では確認モーダルを挟みますが、ステータス変更は再度一括変更すれば元に戻せる操作なので、即時反映にして操作のテンポを優先しました。

適用が完了すると選択を自動的に解除します。選択を維持したままにする設計も可能ですが、そうすると「さっき適用したはずの行に、誤って別のステータスを再適用してしまう」事故が起きやすくなります。安全側に倒して、適用のたびに選択をリセットする挙動にしています。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

0件選択中
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="dt9-wrap">
  <!-- ツールバー(選択件数 + ステータス一括変更) -->
  <div class="dt9-toolbar">
    <span class="dt9-count" id="dt9-count" aria-live="polite">0件選択中</span>
    <div class="dt9-toolbar-actions">
      <select class="dt9-status-select" id="dt9-status-select">
        <option value="" selected>変更先を選択</option>
        <option value="未着手">未着手</option>
        <option value="対応中">対応中</option>
        <option value="完了">完了</option>
      </select>
      <button class="dt9-apply-btn" id="dt9-apply" disabled>適用</button>
      <button class="dt9-clear-btn" id="dt9-clear" disabled>選択を解除</button>
    </div>
  </div>
  <div class="dt9-table-scroll">
    <table class="dt9-table">
      <thead>
        <tr>
          <th class="dt9-check-col">
            <input type="checkbox" id="dt9-select-all" aria-label="すべて選択">
          </th>
          <th class="dt9-id-col">ID</th>
          <th>タスク名</th>
          <th>担当者</th>
          <th>期限</th>
          <th>状態</th>
        </tr>
      </thead>
      <tbody id="dt9-tbody"><!-- 行はJSで動的生成 --></tbody>
    </table>
  </div>
</div>

<script src="./script.js"></script>
</body>
</html>
:root {
  --dt9-primary: #2B7FE8;
  --dt9-border: #E2E8F0;
  --dt9-text: #1A2332;
  --dt9-text-sub: #5A6A7A;
  --dt9-head-bg: #F4F7FF;
  --dt9-selected-bg: #EBF2FF;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  font-family: sans-serif;
  padding: 24px;
  background: #F0F2F5;
  color: var(--dt9-text);
}

.dt9-wrap {
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
}

/* ===== ツールバー ===== */
.dt9-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.dt9-count {
  font-size: 13px;
  font-weight: 600;
  color: var(--dt9-text-sub);
}

.dt9-toolbar-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.dt9-status-select {
  padding: 6px 10px;
  font-size: 13px;
  color: var(--dt9-text);
  background: #fff;
  border: 1.5px solid var(--dt9-border);
  border-radius: 6px;
  cursor: pointer;
  font-family: sans-serif;
}

/* プライマリボタン(適用) */
.dt9-apply-btn {
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 600;
  color: #fff;
  background: var(--dt9-primary);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-family: sans-serif;
  transition: background 0.15s;
}
.dt9-apply-btn:hover:not(:disabled) { background: #1E63C4; }
.dt9-apply-btn:disabled { background: #E2E8F0; color: #A9B4C2; cursor: not-allowed; }

.dt9-clear-btn {
  padding: 6px 14px;
  font-size: 13px;
  color: var(--dt9-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;
}
.dt9-clear-btn:hover:not(:disabled) { background: #F0F6FF; border-color: var(--dt9-primary); }
.dt9-clear-btn:disabled { color: #A9B4C2; border-color: #E2E8F0; background: #F7F9FC; cursor: not-allowed; }

/* ===== テーブル ===== */
.dt9-table-scroll { overflow-x: auto; }

.dt9-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  color: var(--dt9-text);
  background: #fff;
}

.dt9-table th,
.dt9-table td {
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid var(--dt9-border);
  white-space: nowrap;
}

.dt9-table thead th {
  background: var(--dt9-head-bg);
  font-weight: 600;
  color: var(--dt9-text-sub);
}

.dt9-check-col { width: 44px; text-align: center; }
.dt9-id-col { width: 56px; }

.dt9-table input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
  accent-color: var(--dt9-primary);
}

.dt9-table tbody tr:hover td {
  background: #F4F7FF;
  transition: background 0.1s;
}

.dt9-table tbody tr.is-selected td {
  background: var(--dt9-selected-bg);
}

/* ===== 状態バッジ ===== */
.dt9-status {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 9999px;
  font-size: 12px;
  font-weight: 600;
}

.dt9-status[data-status="未着手"] { background: #F3F4F6; color: #6B7280; }
.dt9-status[data-status="対応中"] { background: #DBEAFE; color: #1E40AF; }
.dt9-status[data-status="完了"]   { background: #D1FAE5; color: #065F46; }

/* ===== リセットボタン ===== */
.demo-controls { margin-top: 12px; }

.reset-btn {
  padding: 6px 16px;
  font-size: 13px;
  color: #5A6A7A;
  background: #fff;
  border: 1.5px solid #D0D7E0;
  border-radius: 6px;
  cursor: pointer;
  font-family: sans-serif;
  transition: background 0.15s, border-color 0.15s;
}
.reset-btn:hover { background: #F4F6F9; border-color: #9AA5B4; }

@media (max-width: 600px) {
  .dt9-table th,
  .dt9-table td { padding: 8px 12px; }
  .dt9-toolbar { flex-direction: column; align-items: stretch; }
  .dt9-toolbar-actions { justify-content: flex-start; }
}
// タスク一覧の初期データ(data-table-6 / data-table-8 と同一データ)
const TASKS_INITIAL = [
  { 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: '対応中' }
];

// 描画・操作対象の可変配列(ステータスを書き換えるため let で保持)
let tasks = TASKS_INITIAL.map(function (t) { return Object.assign({}, t); });

// 選択中の行IDを Set で保持する(DOMのcheckedを状態の正としない)
const selected = new Set();

const tbody        = document.getElementById('dt9-tbody');
const selectAll    = document.getElementById('dt9-select-all');
const countEl      = document.getElementById('dt9-count');
const clearBtn     = document.getElementById('dt9-clear');
const statusSelect = document.getElementById('dt9-status-select');
const applyBtn     = document.getElementById('dt9-apply');

// テキストセルを作る(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 = 'dt9-check-col';
    const cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.className = 'dt9-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, 'dt9-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 = 'dt9-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);
  }
  syncChecks();
  syncHighlight();
  updateSelectAllState();
  updateApplyButtonState();
}

// ヘッダーの全選択 → 全ID追加 or 全クリア
function onSelectAll() {
  if (selectAll.checked) {
    tasks.forEach(function (t) { selected.add(t.id); });
  } else {
    selected.clear();
  }
  syncChecks();
  syncHighlight();
  updateSelectAllState();
  updateApplyButtonState();
}

// Set をもとに各行の checked を再同期する
function syncChecks() {
  tbody.querySelectorAll('.dt9-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 {
    selectAll.checked = false;
    selectAll.indeterminate = true;
  }

  countEl.textContent = size + '件選択中';
  clearBtn.disabled = (size === 0);
}

// 「適用」ボタンの活性・非活性を1関数で判定する
// 条件:選択件数が1件以上 かつ プルダウンで変更先が選ばれている
function updateApplyButtonState() {
  applyBtn.disabled = !(selected.size > 0 && statusSelect.value !== '');
}

// 選択のみ解除する(ステータスは変えない)
function clearSelection() {
  selected.clear();
  syncChecks();
  syncHighlight();
  updateSelectAllState();
  updateApplyButtonState();
}

// 「適用」ボタン → 選択中の全行のステータスを一括で書き換える
applyBtn.addEventListener('click', function () {
  const nextStatus = statusSelect.value;
  if (!nextStatus || selected.size === 0) return;

  tasks.forEach(function (t) {
    if (selected.has(t.id)) t.status = nextStatus;
  });

  renderRows();

  // 適用後は選択・プルダウンをすべて初期状態に戻す(誤操作防止)
  selected.clear();
  statusSelect.value = '';
  updateSelectAllState();
  updateApplyButtonState();
});

statusSelect.addEventListener('change', updateApplyButtonState);
selectAll.addEventListener('change', onSelectAll);
clearBtn.addEventListener('click', clearSelection);

// ===== リセット =====
function resetDemo() {
  tasks = TASKS_INITIAL.map(function (t) { return Object.assign({}, t); });
  selected.clear();
  statusSelect.value = '';
  renderRows();
  updateSelectAllState();
  updateApplyButtonState();
}

renderRows();
updateSelectAllState();
updateApplyButtonState();

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や列数変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# テーブルの選択行を一括ステータス変更 作成依頼

## 概要
一覧テーブルで選択した複数行のステータスを、ツールバーのプルダウンと「適用」ボタンでまとめて変更できる一括編集UIを実装してください。題材はタスク一覧です。

## 要件
- 列は「選択(チェックボックス)/ID/タスク名/担当者/期限/状態」。状態は未着手・対応中・完了の色付きバッジで表示する
- 行データはJavaScriptの配列で持ち、行はDOMで動的生成する
- ヘッダーのチェックボックスで全行を一括選択・解除でき、一部選択時はindeterminate(中間状態)で表示する
- ツールバーに「N件選択中」カウンター、ステータス変更プルダウン(未着手/対応中/完了)、「適用」ボタン、「選択を解除」ボタンを配置する
- 「適用」ボタンは、行が1件以上選択されておりプルダウンで変更先が選ばれているときだけ操作可能にする
- 「適用」を押すと、選択中の全行のステータスバッジを選んだ値に一括で書き換える(確認ダイアログは不要)
- 適用が完了したら選択をすべて解除し、プルダウンをプレースホルダーに戻す
- 「選択を解除」ボタンはステータスを変更せず選択のみを解除する
- リセットボタンで、ステータス・選択状態ともに初期状態に戻す

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
選択状態はDOMのcheckedではなく、選択中の行IDを保持するSetで管理してください。
「適用」ボタンの活性・非活性は「選択件数が1件以上」かつ「プルダウンの値が空でない」の2条件をどちらも満たすかで判定してください。
適用処理では、選択中のIDに対応するデータのステータス値を書き換えたうえで再描画し、その後に選択状態とプルダウンの値を初期状態に戻してください。
行・バッジの描画は createElement と textContent で行い、innerHTML に値を結合しないでください。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。