CSV Viewer — CSV読み込み テーブル表示

データ表示 初級

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

CSVファイルをアップロードして内容を一覧確認したい場面は、業務ツールや管理画面で頻繁に発生します。 このページでは、ファイル選択またはドラッグ&ドロップでCSVを読み込み、1行目をヘッダーとしてHTML tableに整形するUIの実装例を紹介します。

FileReader APIを使ってブラウザ上だけで処理するため、サーバーへのアップロードは不要です。 実際にデータをサーバーに登録する処理は、サンプルコードにコメントアウトで記載したFetch APIのコードを参考にしてください。

  • CSVファイル読み込み — クリックまたはドラッグ&ドロップでCSVファイルを選択し、FileReader APIで読み込む(UTF-8エンコーディングのCSVに対応)
  • ヘッダー自動認識 — 1行目をテーブルのヘッダー行として扱い、<thead> に配置する
  • 件数表示・上限通知 — 読み込んだデータ件数をテーブル上部に表示。101件以上は先頭100件のみ表示し通知する
  • 縦スクロール封じ込め — テーブルエリアに max-height + overflow-y: auto を設定。ヘッダー行はスクロール中も固定表示する
  • カンマ内包フィールド対応 — ダブルクォートで囲まれたフィールド内のカンマを正しく解析する

実装のポイント・注意点

このデモが対応しているCSVのエンコーディングは UTF-8 のみです。 Windowsで Excel の「名前を付けて保存」から出力したCSVはShift_JISになっていることが多く、文字化けします。 Excelの場合は「CSV UTF-8(コンマ区切り)」を選んで保存してください。

CSV解析は split(',') だけでは不十分です。 "田中, 太郎" のようにフィールド内にカンマが含まれる場合、単純に分割するとデータが壊れてしまいます。 このサンプルでは1文字ずつ読み進めながら「今ダブルクォートの中か外か」を inQuote フラグで追跡することで、クォート内のカンマを区切り文字として扱わないよう処理しています。

テーブルヘッダーの position: sticky; top: 0 を使うには、親要素に overflow-y: auto が設定されている必要があります。 overflow の指定がないと sticky が効かないため、必ずセットで設定してください。

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

デモ

※ このデモはサーバーへの送信を行いません。選択したCSVの内容をブラウザ上で確認できる擬似デモです。

サンプルソース

3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください。

<!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-viewer">
  <label class="csv-viewer__area" id="upload-area" for="csv-input">
    <svg class="csv-viewer__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
      fill="none" stroke="currentColor" stroke-width="1.5"
      stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
      <polyline points="17 8 12 3 7 8"/>
      <line x1="12" y1="3" x2="12" y2="15"/>
    </svg>
    <span class="csv-viewer__text">クリックまたはドラッグ&ドロップでCSVファイルを選択</span>
    <span class="csv-viewer__sub">.csv ファイル(UTF-8)</span>
    <input type="file" id="csv-input" accept=".csv,text/csv" class="csv-viewer__input">
  </label>

  <div class="csv-viewer__result" id="result-area" hidden>
    <p class="csv-viewer__count" id="csv-count"></p>
    <div class="csv-viewer__scroll">
      <table class="csv-viewer__table">
        <thead id="csv-thead"></thead>
        <tbody id="csv-tbody"></tbody>
      </table>
    </div>
  </div>
</div>

<script src="./script.js"></script>
</body>
</html>
/* CSV読み込み テーブル表示 — style.css */
:root {
  --csv-border:        #C8D5E6;
  --csv-header-bg:     #2B7FE8;
  --csv-header-txt:    #ffffff;
  --csv-stripe-bg:     #F4F7FF;
  --csv-hover-bg:      #EBF2FF;
  --csv-text:          #1A2332;
  --csv-count-color:   #5A6A7A;
}

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

body {
  font-family: sans-serif;
  padding: 24px 16px;
  background: #f8f9fa;
}

.csv-viewer {
  max-width: 720px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* ドロップエリア */
.csv-viewer__area {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 40px 24px;
  border: 2px dashed var(--csv-border);
  border-radius: 12px;
  background: #F7F9FC;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.csv-viewer__area:hover,
.csv-viewer__area.is-dragover {
  border-color: var(--csv-header-bg);
  background: #EBF2FF;
}

.csv-viewer__icon {
  width: 40px;
  height: 40px;
  color: #9AA5B4;
}

.csv-viewer__text {
  font-size: 14px;
  color: #5A6A7A;
  font-weight: 600;
  text-align: center;
}

.csv-viewer__sub {
  font-size: 12px;
  color: #9AA5B4;
}

.csv-viewer__input { display: none; }

/* 件数表示 */
.csv-viewer__count {
  font-size: 13px;
  color: var(--csv-count-color);
  margin: 0 0 8px;
}

/* テーブルの縦スクロールを封じ込める */
.csv-viewer__scroll {
  overflow-x: auto;
  overflow-y: auto;
  max-height: 400px;
  border-radius: 8px;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.10);
}

.csv-viewer__table {
  border-collapse: collapse;
  width: 100%;
  font-size: 14px;
  background: #fff;
  white-space: nowrap;
}

/* ヘッダーをスクロール中も固定する */
.csv-viewer__table thead th {
  position: sticky;
  top: 0;
  background: var(--csv-header-bg);
  color: var(--csv-header-txt);
  padding: 10px 16px;
  text-align: left;
  font-weight: 600;
  z-index: 1;
}

.csv-viewer__table td {
  padding: 9px 16px;
  border-bottom: 1px solid var(--csv-border);
  color: var(--csv-text);
}

.csv-viewer__table tbody tr:last-child td { border-bottom: none; }

/* ストライプ */
.csv-viewer__table tbody tr:nth-child(even) td {
  background: var(--csv-stripe-bg);
}

/* ホバー */
.csv-viewer__table tbody tr:hover td {
  background: var(--csv-hover-bg);
  transition: background 0.1s;
}

@media (max-width: 600px) {
  .csv-viewer__area { padding: 28px 16px; }
  .csv-viewer__table th,
  .csv-viewer__table td { padding: 8px 12px; }
}
// CSV読み込み テーブル表示 — script.js
var MAX_ROWS   = 100;
var uploadArea = document.getElementById('upload-area');
var csvInput   = document.getElementById('csv-input');
var resultArea = document.getElementById('result-area');
var csvCount   = document.getElementById('csv-count');
var csvThead   = document.getElementById('csv-thead');
var csvTbody   = document.getElementById('csv-tbody');

// ファイル選択ダイアログから選んだとき
csvInput.addEventListener('change', function () {
  if (csvInput.files.length > 0) { handleFile(csvInput.files[0]); }
});

// ドラッグ中はエリアをハイライトする
uploadArea.addEventListener('dragover', function (e) {
  e.preventDefault();
  uploadArea.classList.add('is-dragover');
});

uploadArea.addEventListener('dragleave', function () {
  uploadArea.classList.remove('is-dragover');
});

// ドロップ時にファイルを受け取る
uploadArea.addEventListener('drop', function (e) {
  e.preventDefault();
  uploadArea.classList.remove('is-dragover');
  var file = e.dataTransfer.files[0];
  if (file) { handleFile(file); }
});

function handleFile(file) {
  // CSV以外は受け付けない
  var isCSV = file.type === 'text/csv' || file.name.toLowerCase().endsWith('.csv');
  if (!isCSV) {
    alert('CSVファイル(.csv)を選択してください。');
    return;
  }

  // FileReader でUTF-8テキストとして読み込む
  var reader = new FileReader();
  reader.onload = function (e) {
    var rows = parseCSV(e.target.result);
    // 空行を除去する
    rows = rows.filter(function (row) {
      return row.some(function (cell) { return cell !== ''; });
    });
    if (rows.length < 2) {
      alert('データが見つかりませんでした。1行目にヘッダー、2行目以降にデータが含まれるCSVを選択してください。');
      return;
    }
    renderTable(rows);
  };
  reader.readAsText(file, 'UTF-8');

  // --- サーバーに送信する場合 ---
  // var formData = new FormData();
  // formData.append('csv', file);
  // var res = await fetch('/api/import', { method: 'POST', body: formData });
  // var data = await res.json();
  // console.log('インポート完了:', data);
}

// ダブルクォート内のカンマ・改行を正しく処理するCSVパーサー
function parseCSV(text) {
  var rows   = [];
  var row    = [];
  var cell   = '';
  var inQuote = false;

  for (var i = 0; i < text.length; i++) {
    var ch   = text[i];
    var next = text[i + 1];

    if (inQuote) {
      if (ch === '"' && next === '"') {
        // ダブルクォートのエスケープ(""→")
        cell += '"';
        i++;
      } else if (ch === '"') {
        inQuote = false;
      } else {
        cell += ch;
      }
    } else {
      if (ch === '"') {
        inQuote = true;
      } else if (ch === ',') {
        row.push(cell);
        cell = '';
      } else if (ch === '\r' && next === '\n') {
        row.push(cell);
        rows.push(row);
        row  = [];
        cell = '';
        i++;
      } else if (ch === '\n' || ch === '\r') {
        row.push(cell);
        rows.push(row);
        row  = [];
        cell = '';
      } else {
        cell += ch;
      }
    }
  }

  // 最終行を追加する
  if (cell !== '' || row.length > 0) {
    row.push(cell);
    rows.push(row);
  }

  return rows;
}

function renderTable(rows) {
  var headers  = rows[0];
  var dataRows = rows.slice(1);
  var total    = dataRows.length;

  // 101件以上は先頭100件のみ表示する
  if (total > MAX_ROWS) {
    dataRows = dataRows.slice(0, MAX_ROWS);
    csvCount.textContent = total + ' 件中 ' + MAX_ROWS + ' 件を表示しています';
  } else {
    csvCount.textContent = total + ' 件';
  }

  // ヘッダー行を生成する
  csvThead.innerHTML = '';
  var theadRow = document.createElement('tr');
  headers.forEach(function (label) {
    var th = document.createElement('th');
    th.textContent = label;
    theadRow.appendChild(th);
  });
  csvThead.appendChild(theadRow);

  // データ行を生成する
  csvTbody.innerHTML = '';
  dataRows.forEach(function (cells) {
    var tr = document.createElement('tr');
    headers.forEach(function (_, colIndex) {
      var td = document.createElement('td');
      td.textContent = cells[colIndex] != null ? cells[colIndex] : '';
      tr.appendChild(td);
    });
    csvTbody.appendChild(tr);
  });

  resultArea.hidden = false;
}

function resetDemo() {
  csvInput.value = '';
  csvThead.innerHTML = '';
  csvTbody.innerHTML = '';
  csvCount.textContent = '';
  resultArea.hidden = true;
}

AI用プロンプト

このプロンプトをChatGPTやClaudeに渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。

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

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

# CSV読み込み テーブル表示 作成依頼

## 概要
CSVファイルをブラウザで読み込み、HTML tableで一覧表示するUIを実装してください。
ファイル選択またはドラッグ&ドロップでCSVを受け取り、1行目をヘッダーとして解析します。

## 要件
- クリックでファイル選択ダイアログを開けること
- エリアへのドラッグ&ドロップでもCSVを読み込めること
- 1行目をヘッダー行として thead に表示すること
- 2行目以降をデータ行として tbody に表示すること
- 読み込んだデータ件数をテーブル上部に表示すること
- データが101件以上の場合は先頭100件のみ表示し「N 件中 100 件を表示」と通知すること
- ダブルクォートで囲まれたフィールド内のカンマを正しく解析すること
- CSV以外のファイルが選択された場合はエラーメッセージを表示すること
- リセットボタンで初期状態に戻せること
- テーブルエリアに max-height + overflow-y: auto を設定し、縦に長くなりすぎないようにすること
- ヘッダー行を sticky で固定し、スクロールしても列名が見えるようにすること
- テーブルが横に長い場合は横スクロールで対応すること
- ファイルはUTF-8エンコーディングのCSVのみ対応とし、その旨をUIに明記すること

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- FileReader API を使用してブラウザ上でCSVを読み込む
- レスポンシブ対応:必要

## 動作詳細
FileReader の readAsText(file, 'UTF-8') でCSVテキストを読み込む。対応エンコーディングはUTF-8のみ。
parseCSV() 関数で状態マシン型のパーサーを実装し、ダブルクォート内のカンマを区切り文字として扱わないよう処理する。
データが101件以上の場合は slice(0, 100) で先頭100件に切り詰め、件数表示を「N 件中 100 件を表示」に変更する。
renderTable() で createElement + appendChild を使ってテーブルを動的生成する。
textContent のみ使用し、innerHTML に変数を直接渡さない(XSS対策)。
サーバーにCSVデータを送信する場合の実装はコメントアウトで記載すること(FormData + fetch を使った例)。

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