ソート可能テーブル
列見出しをクリックすると昇順・降順で並び替わるソート機能付きテーブル。数値・文字列・日付のどの列でも矢印インジケータで並び順が確認できます。
ライブデモ
コード
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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。