CSVエクスポートボタン(CSV Export)— フィルタ後の表示行をCSVダウンロード

ユーティリティ 初級

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

業務アプリでは「今画面に表示されているデータをそのままCSVで欲しい」という要求がよくあります。このページでは、テーブルをフィルタ・絞り込んだ後の表示行をそのままCSVダウンロードする実装例を紹介します。BlobURL.createObjectURL を使ってサーバーへの再リクエストなしにクライアント完結で処理できるため、バックエンドAPIの実装が不要です。フィルタ状態とCSV出力内容が一致するので、「エクスポートしたら画面と違うデータが入っていた」というトラブルが起きません。

  • ステータスフィルタ — セレクトボックスでステータス(全件・処理中・完了・キャンセル)を絞り込み、テーブルの表示行を切り替える
  • 表示行のみエクスポート — 「CSVエクスポート」ボタンで現在テーブルに表示されている行だけをCSVに変換してダウンロードする
  • クォート処理 — セル値にカンマ・ダブルクォート・改行が含まれる場合、RFC 4180準拠の "..." でエスケープして出力する
  • Blobメモリ解放URL.revokeObjectURL() をダウンロード直後に呼び出してメモリリークを防ぐ
  • ダウンロードファイル名 — 出力時の日付を含むファイル名(例: orders_20260615.csv)を自動生成する

実装のポイント・注意点

URL.createObjectURL(blob) で生成したURLは、明示的に URL.revokeObjectURL(url) を呼ばないとページを閉じるまでメモリに居座ります。ダウンロードの疑似クリック直後に呼び出すのがベストプラクティスです。<a> 要素のクリック後にブラウザがダウンロードをキューに積む仕組みのため、即時解放しても問題ありません。

CSV出力時のエスケープはRFC 4180に従い、カンマ(,)・ダブルクォート(")・改行(\n/\r)のいずれかを含むセルを "..." で囲みます。ダブルクォート自体は ""(2つ連続)にエスケープします。このルールを省くと、住所や備考欄にカンマが入った際にCSVが壊れます。

今回はUTF-8(BOMなし)で出力します。Excelで直接開くと文字化けする場合がありますが、「データ」→「テキストから」でインポート時にUTF-8を指定すれば正しく読み込めます。数万件超のデータを扱う場合はサーバーサイドAPIでのCSV生成・ストリーム配信が推奨です。

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

デモ

注文ID 注文日 顧客名 商品名 金額 ステータス

10 件表示中

サンプルソース

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>CSVエクスポートボタン</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="csv-export-wrap">
  <div class="csv-toolbar">
    <label for="statusFilter" class="csv-filter-label">ステータス</label>
    <select id="statusFilter" class="csv-filter-select" onchange="applyFilter()">
      <option value="">全件</option>
      <option value="処理中">処理中</option>
      <option value="完了">完了</option>
      <option value="キャンセル">キャンセル</option>
    </select>
    <button class="csv-export-btn" onclick="exportCsv()">CSVエクスポート</button>
  </div>

  <div class="csv-table-wrap">
    <table class="csv-table">
      <thead>
        <tr>
          <th>注文ID</th>
          <th>注文日</th>
          <th>顧客名</th>
          <th>商品名</th>
          <th>金額</th>
          <th>ステータス</th>
        </tr>
      </thead>
      <tbody id="orderTableBody">
        <!-- JS で描画 -->
      </tbody>
    </table>
    <p id="emptyMessage" class="csv-empty" hidden>該当する注文がありません。</p>
  </div>

  <p class="csv-count"><span id="rowCount">10</span> 件表示中</p>
</div>

<script src="./script.js"></script>
</body>
</html>
:root {
  --primary: #2B7FE8;
  --primary-dark: #1a6fd4;
  --border: #D0D7E0;
  --row-hover: #F4F6F9;
  --text-sub: #5A6A7A;
  --badge-processing-bg: #FEF9C3;
  --badge-processing-text: #854D0E;
  --badge-done-bg: #DCFCE7;
  --badge-done-text: #166534;
  --badge-cancelled-bg: #F1F5F9;
  --badge-cancelled-text: #5A6A7A;
}

* { box-sizing: border-box; }

body {
  font-family: sans-serif;
  font-size: 14px;
  color: #1a1a2e;
  background: #f8fafc;
  padding: 24px;
  margin: 0;
}

/* ── ツールバー ── */
.csv-export-wrap {
  max-width: 800px;
  margin: 0 auto;
}

.csv-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.csv-filter-label {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-sub);
}

.csv-filter-select {
  border: 1.5px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 14px;
  font-family: inherit;
  background: #fff;
  cursor: pointer;
}

.csv-filter-select:focus {
  outline: none;
  border-color: var(--primary);
}

.csv-export-btn {
  padding: 8px 20px;
  font-size: 14px;
  font-family: inherit;
  font-weight: 600;
  color: #fff;
  background: var(--primary);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.csv-export-btn:hover {
  background: var(--primary-dark);
}

/* ── テーブル ── */
.csv-table-wrap {
  overflow-x: auto;
}

.csv-table {
  width: 100%;
  border-collapse: collapse;
}

.csv-table th {
  background: var(--primary);
  color: #fff;
  padding: 10px 12px;
  text-align: left;
  font-size: 13px;
  white-space: nowrap;
}

.csv-table td {
  padding: 10px 12px;
  border-bottom: 1px solid #E8EDF2;
  white-space: nowrap;
}

.csv-table tbody tr:hover {
  background: var(--row-hover);
}

/* ── ステータスバッジ ── */
.status-badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
}

.status-badge--processing {
  background: var(--badge-processing-bg);
  color: var(--badge-processing-text);
}

.status-badge--done {
  background: var(--badge-done-bg);
  color: var(--badge-done-text);
}

.status-badge--cancelled {
  background: var(--badge-cancelled-bg);
  color: var(--badge-cancelled-text);
}

/* ── 件数表示・空メッセージ ── */
.csv-count {
  text-align: right;
  font-size: 13px;
  color: var(--text-sub);
  margin-top: 8px;
  margin-bottom: 0;
}

.csv-empty {
  text-align: center;
  color: var(--text-sub);
  padding: 32px 0;
  margin: 0;
}

.csv-empty[hidden] {
  display: none;
}
// 注文データ(サンプル)
const orders = [
  { id: 'ORD-001', date: '2026-06-01', customer: '田中 太郎', item: 'Webデザインテンプレート', amount: 4800,  status: '完了' },
  { id: 'ORD-002', date: '2026-06-02', customer: '鈴木 花子', item: 'UIコンポーネントセット',   amount: 9800,  status: '処理中' },
  { id: 'ORD-003', date: '2026-06-03', customer: '佐藤 次郎', item: 'ランディングページ素材',   amount: 2980,  status: 'キャンセル' },
  { id: 'ORD-004', date: '2026-06-04', customer: '山田 三郎', item: 'Webデザインテンプレート', amount: 4800,  status: '完了' },
  { id: 'ORD-005', date: '2026-06-05', customer: '伊藤 四郎', item: 'UIコンポーネントセット',   amount: 9800,  status: '完了' },
  { id: 'ORD-006', date: '2026-06-07', customer: '渡辺 五郎', item: 'ランディングページ素材',   amount: 2980,  status: '処理中' },
  { id: 'ORD-007', date: '2026-06-08', customer: '小林 六子', item: 'Webデザインテンプレート', amount: 4800,  status: '処理中' },
  { id: 'ORD-008', date: '2026-06-10', customer: '加藤 七郎', item: 'UIコンポーネントセット',   amount: 9800,  status: 'キャンセル' },
  { id: 'ORD-009', date: '2026-06-12', customer: '吉田 八郎', item: 'ランディングページ素材',   amount: 2980,  status: '完了' },
  { id: 'ORD-010', date: '2026-06-14', customer: '山口 九子', item: 'Webデザインテンプレート', amount: 4800,  status: '処理中' },
];

// 現在フィルタ後の行を保持する変数
let filteredOrders = [];

// テーブルを描画する
function renderTable(rows) {
  const tbody = document.getElementById('orderTableBody');
  const empty = document.getElementById('emptyMessage');
  const count = document.getElementById('rowCount');

  // 行をクリアして再描画
  tbody.innerHTML = '';

  if (rows.length === 0) {
    empty.hidden = false;
  } else {
    empty.hidden = true;
    rows.forEach(function(row) {
      const tr = document.createElement('tr');

      // 各セルを textContent で安全に挿入
      const cells = [
        row.id,
        row.date,
        row.customer,
        row.item,
        '¥' + row.amount.toLocaleString(),
      ];
      cells.forEach(function(val) {
        const td = document.createElement('td');
        td.textContent = val;
        tr.appendChild(td);
      });

      // ステータスバッジのみ span を生成して挿入
      const statusTd = document.createElement('td');
      const badge = document.createElement('span');
      badge.textContent = row.status;
      badge.className = 'status-badge ' + getStatusClass(row.status);
      statusTd.appendChild(badge);
      tr.appendChild(statusTd);

      tbody.appendChild(tr);
    });
  }

  count.textContent = rows.length;
}

// ステータスに応じた CSS クラスを返す
function getStatusClass(status) {
  if (status === '処理中') return 'status-badge--processing';
  if (status === '完了')   return 'status-badge--done';
  return 'status-badge--cancelled';
}

// フィルタを適用してテーブルを更新する
function applyFilter() {
  const val = document.getElementById('statusFilter').value;
  filteredOrders = val ? orders.filter(o => o.status === val) : orders.slice();
  renderTable(filteredOrders);
}

// CSVの1行文字列を生成する(RFC 4180準拠)
function toCsvRow(fields) {
  return fields.map(function(field) {
    let str = String(field);
    // カンマ・ダブルクォート・改行を含む場合はクォートで囲む
    if (/[,"\n\r]/.test(str)) {
      str = '"' + str.replace(/"/g, '""') + '"';
    }
    return str;
  }).join(',');
}

// 今日の日付を YYYYMMDD 形式で返す
function getDateStr() {
  const d = new Date();
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return y + m + day;
}

// CSVをダウンロードする
// 注: UTF-8(BOMなし)で出力。Excelで開く場合はインポート時にUTF-8を指定してください。
function exportCsv() {
  // ヘッダー行
  const header = toCsvRow(['注文ID', '注文日', '顧客名', '商品名', '金額', 'ステータス']);

  // データ行(現在表示中の行のみ)
  const rows = filteredOrders.map(o =>
    toCsvRow([o.id, o.date, o.customer, o.item, o.amount, o.status])
  );

  const csvString = [header, ...rows].join('\n');

  // Blob を生成してダウンロードリンクを疑似クリック
  const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'orders_' + getDateStr() + '.csv';
  a.click();

  // ダウンロード後すぐに Blob URL を解放してメモリリークを防ぐ
  URL.revokeObjectURL(url);
}

// 初期描画
applyFilter();

AI用プロンプト

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

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

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

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

# CSVエクスポートボタン 作成依頼

## 概要
テーブルのデータをCSVファイルとしてダウンロードするボタンを実装してください。
ステータスフィルタで絞り込んだ後の表示行そのままをエクスポートします。

## 要件
- 注文履歴テーブル(注文ID / 注文日 / 顧客名 / 商品名 / 金額 / ステータス)を表示すること
- ステータスのセレクトボックスで表示行を絞り込めること(全件 / 処理中 / 完了 / キャンセル)
- 「CSVエクスポート」ボタンで現在表示中の行のみをCSVとしてダウンロードできること
- ダウンロードファイル名は `orders_YYYYMMDD.csv`(実行日付を含む)にすること
- セルにカンマ・改行・ダブルクォートが含まれる場合 "..." でクォートすること(RFC 4180準拠)
- Blob URLは `URL.revokeObjectURL()` でダウンロード直後に解放すること
- 外部ライブラリは使用しないこと

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(テーブルは横スクロール対応)

## 動作詳細
テーブルのデータはJSの配列(orders)でハードコードして管理する。
ステータスフィルタが変わるたびに配列を絞り込み、filteredOrders に保持してテーブルを再描画する。
「CSVエクスポート」ボタンをクリックすると、filteredOrders の各行をCSV文字列に変換する。
new Blob([csvString], { type: 'text/csv;charset=utf-8' }) でBlobを生成(UTF-8・BOMなし)。
URL.createObjectURL() でダウンロードリンクを生成し、<a download="..."> を疑似クリックしてダウンロードを開始する。
ダウンロード後は即座に URL.revokeObjectURL() を呼んでメモリを解放する。

サンプルデータ(10件):
- ORD-001 / 2026-06-01 / 田中 太郎 / Webデザインテンプレート / 4,800円 / 完了
- ORD-002 / 2026-06-02 / 鈴木 花子 / UIコンポーネントセット / 9,800円 / 処理中
- ORD-003 / 2026-06-03 / 佐藤 次郎 / ランディングページ素材 / 2,980円 / キャンセル
- ORD-004 / 2026-06-04 / 山田 三郎 / Webデザインテンプレート / 4,800円 / 完了
- ORD-005 / 2026-06-05 / 伊藤 四郎 / UIコンポーネントセット / 9,800円 / 完了
- ORD-006 / 2026-06-07 / 渡辺 五郎 / ランディングページ素材 / 2,980円 / 処理中
- ORD-007 / 2026-06-08 / 小林 六子 / Webデザインテンプレート / 4,800円 / 処理中
- ORD-008 / 2026-06-10 / 加藤 七郎 / UIコンポーネントセット / 9,800円 / キャンセル
- ORD-009 / 2026-06-12 / 吉田 八郎 / ランディングページ素材 / 2,980円 / 完了
- ORD-010 / 2026-06-14 / 山口 九子 / Webデザインテンプレート / 4,800円 / 処理中

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