実装の解説
全体構造
このテーブルは2つの独立したテーブルをFlexboxで横並びにすることで、左列固定・右列スクロール可能を実現しています。
┌─────────────────────────────────────┐
│ table-wrapper (display: flex) │
│ ┌──────────┬────────────────────┐ │
│ │ 固定列 │ スクロール可能列 │ │
│ │ table- │ table-scroll │ │
│ │ fixed │ (overflow-x: auto) │ │
│ └──────────┴────────────────────┘ │
└─────────────────────────────────────┘
HTMLの構造
1. 親コンテナ(Flexbox)
<div class="table-wrapper">
<!-- 2つのテーブルを横並びに配置 -->
</div>
2. 左側:固定列テーブル
<div class="table-fixed">
<table class="price-table-left">
<thead>
<tr>
<th>コーヒー豆の品名</th>
</tr>
</thead>
<tbody>
<tr><td>ブラジリアン</td></tr>
<tr><td>タンザ</td></tr>
<!-- 7行分 -->
</tbody>
</table>
</div>
1列のみ(品名だけ)
スクロールしても画面に固定される
3. 右側:スクロール可能テーブル
<div class="table-scroll">
<table class="price-table-right">
<thead>
<tr>
<th rowspan="2">豆色</th>
<th rowspan="2">産地</th>
<th colspan="8">ブロック料金</th>
<th rowspan="2">1カ月購入<br>4500g</th>
</tr>
<tr>
<!-- colspan分の詳細 -->
<th>500g</th>
<th>1000g</th>
<!-- ... 8列 -->
</tr>
</thead>
<tbody>
<tr>
<td>明るい</td>
<td>サンパウロ</td>
<td>900円</td>
<!-- ... 9列分の価格 -->
</tr>
<!-- 7行分 -->
</tbody>
</table>
</div>
CSSの設計
1. Flexboxで横並び
.table-wrapper {
display: flex;
width: 100%;
}
→ 2つのテーブルを横に配置
2. 左側を固定
.table-fixed {
flex-shrink: 0; /* 縮小禁止 */
}
→ 左テーブルの幅は変わらない
3. 右側をスクロール可能に
.table-scroll {
flex-grow: 1; /* 残りのスペースを使う */
overflow-x: auto; /* 横スクロール有効 */
scrollbar-width: none; /* スクロールバー非表示(Firefox) */
-ms-overflow-style: none; /* IE/Edge */
}
.table-scroll::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
4. 高さの統一
.price-table-left thead tr:first-child th {
height: 155px; /* 右側の2行分と同じ高さ */
vertical-align: middle;
}
5. 共通スタイル
.price-table-left,
.price-table-right {
border-collapse: collapse;
font-size: 14px;
}
.price-table-left tbody td,
.price-table-right tbody td {
padding: 12px 10px;
border: 1px solid #ddd;
}
*rowspan/colspanの使い方
rowspan="2"(2行にまたがる)
<th rowspan="2">豆色</th>
<th rowspan="2">産地</th>
→ ヘッダーの1行目と2行目を縦に結合
colspan="8"(8列にまたがる)
<th colspan="8">ブロック料金</th>
→ 8列分を横に結合してタイトル表示
JavaScriptのドラッグスクロール
// テーブルのドラッグスクロール機能
document.addEventListener("DOMContentLoaded", function () {
const scrollContainer = document.querySelector(".table-scroll");
if (!scrollContainer) return;
let isDown = false;
let startX;
let scrollLeft;
// マウスカーソルをグラブ(手のひら)に変更
scrollContainer.style.cursor = "grab";
scrollContainer.addEventListener("mousedown", (e) => {
isDown = true;
scrollContainer.style.cursor = "grabbing";
startX = e.pageX - scrollContainer.offsetLeft;
scrollLeft = scrollContainer.scrollLeft;
});
scrollContainer.addEventListener("mouseleave", () => {
isDown = false;
scrollContainer.style.cursor = "grab";
});
scrollContainer.addEventListener("mouseup", () => {
isDown = false;
scrollContainer.style.cursor = "grab";
});
scrollContainer.addEventListener("mousemove", (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - scrollContainer.offsetLeft;
const walk = (x - startX) * 2; // スクロール速度(数値を大きくすると速くなる)
scrollContainer.scrollLeft = scrollLeft - walk;
});
});
動作:
テーブル上でマウスクリック
左右にドラッグ
スクロール位置が連動して移動
カーソルはgrab(開いた手)→ grabbing(掴んでいる手)に変化
なぜこの方法が優れているか
❌ position: sticky の問題点
ブラウザ互換性が不安定
border-collapseと相性が悪い
複雑なヘッダー(rowspan/colspan)で崩れる
✅ 2テーブル分割の利点
確実に動作(全ブラウザ対応)
スクロール制御が簡単(overflow-xだけ)
レスポンシブ対応しやすい
独立したスタイル管理が可能
レスポンシブ対応
@media screen and (max-width: 768px) {
.price-table-left thead tr:first-child th {
height: 128px; /* スマホでは低く */
}
.price-table-left, .price-table-right {
font-size: 12px; /* 文字を小さく */
}
}
まとめ
この実装の核心は:
テーブルを2つに分割(固定列 + スクロール列)
Flexboxで横並び(display: flex)
行数とセルサイズを完全一致させる
JavaScriptでドラッグスクロール追加
→ 左列が常に見える使いやすいテーブルが完成
下が一層table