ソート可能テーブル

列見出しをクリックすると昇順・降順で並び替わるソート機能付きテーブル。数値・文字列・日付のどの列でも矢印インジケータで並び順が確認できます。

#css#js#table

ライブデモ

コード

HTML
<!-- ソート可能テーブル: 列ヘッダークリックで昇順/降順を切り替え -->
<section class="tb3-wrap">
  <div class="tb3-card">
    <header class="tb3-head">
      <h2 class="tb3-title">商品マスタ</h2>
      <p class="tb3-hint">見出しをクリックして並び替え</p>
    </header>
    <div class="tb3-scroll">
      <table class="tb3-table">
        <caption class="sr-only">商品の価格・在庫・登録日。列見出しクリックで並び替え可能</caption>
        <thead>
          <tr>
            <th scope="col" data-key="name" data-type="text" tabindex="0" role="button" aria-sort="none">商品名</th>
            <th scope="col" data-key="category" data-type="text" tabindex="0" role="button" aria-sort="none">カテゴリ</th>
            <th scope="col" class="tb3-num" data-key="price" data-type="number" tabindex="0" role="button" aria-sort="none">価格</th>
            <th scope="col" class="tb3-num" data-key="stock" data-type="number" tabindex="0" role="button" aria-sort="none">在庫</th>
            <th scope="col" data-key="date" data-type="date" tabindex="0" role="button" aria-sort="none">登録日</th>
          </tr>
        </thead>
        <tbody>
          <tr><th scope="row">ノートPC 13インチ</th><td>家電</td><td class="tb3-num">¥128,000</td><td class="tb3-num">12</td><td>2026-01-15</td></tr>
          <tr><th scope="row">ワイヤレスマウス</th><td>家電</td><td class="tb3-num">¥3,200</td><td class="tb3-num">145</td><td>2026-03-02</td></tr>
          <tr><th scope="row">デスクライト</th><td>インテリア</td><td class="tb3-num">¥5,400</td><td class="tb3-num">67</td><td>2026-02-20</td></tr>
          <tr><th scope="row">オフィスチェア</th><td>家具</td><td class="tb3-num">¥24,800</td><td class="tb3-num">21</td><td>2025-11-08</td></tr>
          <tr><th scope="row">観葉植物(小)</th><td>インテリア</td><td class="tb3-num">¥1,800</td><td class="tb3-num">88</td><td>2026-04-11</td></tr>
          <tr><th scope="row">ホワイトボード</th><td>文具</td><td class="tb3-num">¥6,900</td><td class="tb3-num">34</td><td>2025-12-30</td></tr>
          <tr><th scope="row">コーヒーメーカー</th><td>キッチン</td><td class="tb3-num">¥9,800</td><td class="tb3-num">19</td><td>2026-05-19</td></tr>
          <tr><th scope="row">加湿器</th><td>家電</td><td class="tb3-num">¥7,600</td><td class="tb3-num">52</td><td>2026-01-27</td></tr>
        </tbody>
      </table>
    </div>
  </div>
</section>
CSS
/* ソート可能テーブル: 列ヘッダークリックで昇順/降順・矢印インジケータ表示 */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 28px 16px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, -apple-system, sans-serif;
  background: linear-gradient(160deg, #f7f8fc 0%, #eef0f7 100%);
  color: #1c2033;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.tb3-wrap { width: 100%; display: grid; place-items: center; }
.tb3-card {
  width: min(760px, 100%);
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 60px rgba(24, 28, 55, .10), 0 2px 8px rgba(24, 28, 55, .04);
  overflow: hidden;
}
.tb3-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 20px 24px; border-bottom: 1px solid #eef0f6;
}
.tb3-title { margin: 0; font-size: 17px; font-weight: 800; }
.tb3-hint { margin: 0; font-size: 12px; color: #8b91a7; }

.tb3-scroll { width: 100%; overflow-x: auto; }
.tb3-table { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 13.5px; }

.tb3-table thead th {
  text-align: left; padding: 12px 20px; background: #12172b; color: #fff; font-weight: 700;
  font-size: 12.5px; letter-spacing: .02em; white-space: nowrap; cursor: pointer; user-select: none;
  transition: background .15s ease;
}
.tb3-table thead th:hover { background: #1c2340; }
.tb3-table thead th:focus-visible { outline: 2px solid #a5b4fc; outline-offset: -2px; }
.tb3-table thead th.tb3-num { text-align: right; }
.tb3-table thead th::after { content: '\21C5'; display: inline-block; margin-left: 6px; font-size: 10px; opacity: .45; }
.tb3-table thead th[aria-sort="ascending"]::after { content: '\25B2'; opacity: 1; color: #8ea2ff; }
.tb3-table thead th[aria-sort="descending"]::after { content: '\25BC'; opacity: 1; color: #8ea2ff; }

.tb3-table tbody th, .tb3-table tbody td {
  padding: 12px 20px; border-bottom: 1px solid #eef0f6; white-space: nowrap; text-align: left; font-weight: 400;
}
.tb3-table tbody th { font-weight: 600; color: #1c2033; }
.tb3-table tbody tr:last-child th, .tb3-table tbody tr:last-child td { border-bottom: none; }
.tb3-table tbody tr:hover { background: #f7f8ff; }
.tb3-num { text-align: right; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) { .tb3-table thead th { transition: none; } }
JavaScript
// ソート可能テーブル: 列ヘッダー(th[data-key])クリックで昇順/降順を切り替え、数値/日付/文字列を型ごとに比較する
(() => {
  const table = document.querySelector('.tb3-table');
  if (!table) return;
  const thead = table.querySelector('thead');
  const tbody = table.querySelector('tbody');
  if (!thead || !tbody) return;
  const ths = Array.from(thead.querySelectorAll('th[data-key]'));
  if (!ths.length) return;

  const state = { key: null, dir: 1 };

  function parseVal(cell, type) {
    const raw = (cell && cell.textContent || '').trim();
    if (type === 'number') {
      const n = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
      return Number.isNaN(n) ? -Infinity : n;
    }
    if (type === 'date') {
      const t = Date.parse(raw);
      return Number.isNaN(t) ? 0 : t;
    }
    return raw;
  }

  function sortBy(th) {
    const key = th.dataset.key;
    const type = th.dataset.type || 'text';
    const idx = ths.indexOf(th);
    state.dir = state.key === key ? -state.dir : 1;
    state.key = key;

    const rows = Array.from(tbody.querySelectorAll('tr'));
    rows.sort((a, b) => {
      const av = parseVal(a.children[idx], type);
      const bv = parseVal(b.children[idx], type);
      if (av < bv) return -1 * state.dir;
      if (av > bv) return 1 * state.dir;
      return 0;
    });
    rows.forEach((r) => tbody.appendChild(r));

    ths.forEach((t) => {
      t.setAttribute('aria-sort', 'none');
      t.classList.remove('is-asc', 'is-desc');
    });
    th.setAttribute('aria-sort', state.dir === 1 ? 'ascending' : 'descending');
    th.classList.add(state.dir === 1 ? 'is-asc' : 'is-desc');
  }

  ths.forEach((th) => {
    th.addEventListener('click', () => sortBy(th));
    th.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); sortBy(th); }
    });
  });
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ソート可能テーブル」の効果を追加してください。

# 追加してほしい効果
ソート可能テーブル(カード & テーブル)
列見出しをクリックすると昇順・降順で並び替わるソート機能付きテーブル。数値・文字列・日付のどの列でも矢印インジケータで並び順が確認できます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ソート可能テーブル: 列ヘッダークリックで昇順/降順を切り替え -->
<section class="tb3-wrap">
  <div class="tb3-card">
    <header class="tb3-head">
      <h2 class="tb3-title">商品マスタ</h2>
      <p class="tb3-hint">見出しをクリックして並び替え</p>
    </header>
    <div class="tb3-scroll">
      <table class="tb3-table">
        <caption class="sr-only">商品の価格・在庫・登録日。列見出しクリックで並び替え可能</caption>
        <thead>
          <tr>
            <th scope="col" data-key="name" data-type="text" tabindex="0" role="button" aria-sort="none">商品名</th>
            <th scope="col" data-key="category" data-type="text" tabindex="0" role="button" aria-sort="none">カテゴリ</th>
            <th scope="col" class="tb3-num" data-key="price" data-type="number" tabindex="0" role="button" aria-sort="none">価格</th>
            <th scope="col" class="tb3-num" data-key="stock" data-type="number" tabindex="0" role="button" aria-sort="none">在庫</th>
            <th scope="col" data-key="date" data-type="date" tabindex="0" role="button" aria-sort="none">登録日</th>
          </tr>
        </thead>
        <tbody>
          <tr><th scope="row">ノートPC 13インチ</th><td>家電</td><td class="tb3-num">¥128,000</td><td class="tb3-num">12</td><td>2026-01-15</td></tr>
          <tr><th scope="row">ワイヤレスマウス</th><td>家電</td><td class="tb3-num">¥3,200</td><td class="tb3-num">145</td><td>2026-03-02</td></tr>
          <tr><th scope="row">デスクライト</th><td>インテリア</td><td class="tb3-num">¥5,400</td><td class="tb3-num">67</td><td>2026-02-20</td></tr>
          <tr><th scope="row">オフィスチェア</th><td>家具</td><td class="tb3-num">¥24,800</td><td class="tb3-num">21</td><td>2025-11-08</td></tr>
          <tr><th scope="row">観葉植物(小)</th><td>インテリア</td><td class="tb3-num">¥1,800</td><td class="tb3-num">88</td><td>2026-04-11</td></tr>
          <tr><th scope="row">ホワイトボード</th><td>文具</td><td class="tb3-num">¥6,900</td><td class="tb3-num">34</td><td>2025-12-30</td></tr>
          <tr><th scope="row">コーヒーメーカー</th><td>キッチン</td><td class="tb3-num">¥9,800</td><td class="tb3-num">19</td><td>2026-05-19</td></tr>
          <tr><th scope="row">加湿器</th><td>家電</td><td class="tb3-num">¥7,600</td><td class="tb3-num">52</td><td>2026-01-27</td></tr>
        </tbody>
      </table>
    </div>
  </div>
</section>

【CSS】
/* ソート可能テーブル: 列ヘッダークリックで昇順/降順・矢印インジケータ表示 */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 28px 16px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, -apple-system, sans-serif;
  background: linear-gradient(160deg, #f7f8fc 0%, #eef0f7 100%);
  color: #1c2033;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.tb3-wrap { width: 100%; display: grid; place-items: center; }
.tb3-card {
  width: min(760px, 100%);
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 60px rgba(24, 28, 55, .10), 0 2px 8px rgba(24, 28, 55, .04);
  overflow: hidden;
}
.tb3-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 20px 24px; border-bottom: 1px solid #eef0f6;
}
.tb3-title { margin: 0; font-size: 17px; font-weight: 800; }
.tb3-hint { margin: 0; font-size: 12px; color: #8b91a7; }

.tb3-scroll { width: 100%; overflow-x: auto; }
.tb3-table { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 13.5px; }

.tb3-table thead th {
  text-align: left; padding: 12px 20px; background: #12172b; color: #fff; font-weight: 700;
  font-size: 12.5px; letter-spacing: .02em; white-space: nowrap; cursor: pointer; user-select: none;
  transition: background .15s ease;
}
.tb3-table thead th:hover { background: #1c2340; }
.tb3-table thead th:focus-visible { outline: 2px solid #a5b4fc; outline-offset: -2px; }
.tb3-table thead th.tb3-num { text-align: right; }
.tb3-table thead th::after { content: '\21C5'; display: inline-block; margin-left: 6px; font-size: 10px; opacity: .45; }
.tb3-table thead th[aria-sort="ascending"]::after { content: '\25B2'; opacity: 1; color: #8ea2ff; }
.tb3-table thead th[aria-sort="descending"]::after { content: '\25BC'; opacity: 1; color: #8ea2ff; }

.tb3-table tbody th, .tb3-table tbody td {
  padding: 12px 20px; border-bottom: 1px solid #eef0f6; white-space: nowrap; text-align: left; font-weight: 400;
}
.tb3-table tbody th { font-weight: 600; color: #1c2033; }
.tb3-table tbody tr:last-child th, .tb3-table tbody tr:last-child td { border-bottom: none; }
.tb3-table tbody tr:hover { background: #f7f8ff; }
.tb3-num { text-align: right; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) { .tb3-table thead th { transition: none; } }

【JavaScript】
// ソート可能テーブル: 列ヘッダー(th[data-key])クリックで昇順/降順を切り替え、数値/日付/文字列を型ごとに比較する
(() => {
  const table = document.querySelector('.tb3-table');
  if (!table) return;
  const thead = table.querySelector('thead');
  const tbody = table.querySelector('tbody');
  if (!thead || !tbody) return;
  const ths = Array.from(thead.querySelectorAll('th[data-key]'));
  if (!ths.length) return;

  const state = { key: null, dir: 1 };

  function parseVal(cell, type) {
    const raw = (cell && cell.textContent || '').trim();
    if (type === 'number') {
      const n = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
      return Number.isNaN(n) ? -Infinity : n;
    }
    if (type === 'date') {
      const t = Date.parse(raw);
      return Number.isNaN(t) ? 0 : t;
    }
    return raw;
  }

  function sortBy(th) {
    const key = th.dataset.key;
    const type = th.dataset.type || 'text';
    const idx = ths.indexOf(th);
    state.dir = state.key === key ? -state.dir : 1;
    state.key = key;

    const rows = Array.from(tbody.querySelectorAll('tr'));
    rows.sort((a, b) => {
      const av = parseVal(a.children[idx], type);
      const bv = parseVal(b.children[idx], type);
      if (av < bv) return -1 * state.dir;
      if (av > bv) return 1 * state.dir;
      return 0;
    });
    rows.forEach((r) => tbody.appendChild(r));

    ths.forEach((t) => {
      t.setAttribute('aria-sort', 'none');
      t.classList.remove('is-asc', 'is-desc');
    });
    th.setAttribute('aria-sort', state.dir === 1 ? 'ascending' : 'descending');
    th.classList.add(state.dir === 1 ? 'is-asc' : 'is-desc');
  }

  ths.forEach((th) => {
    th.addEventListener('click', () => sortBy(th));
    th.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); sortBy(th); }
    });
  });
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。