HTMLの <table> 要素で使える <thead>・<tbody>・<tfoot> は、テーブルの行を意味的にグループ化するためのセクショニング要素です。
「見た目は変わらないから省略している」「とりあえず <tbody> だけ書いている」という方は多いかもしれません。しかし、これらの要素を正しく使い分けることで、アクセシビリティ・CSSスタイリング・JavaScript操作・印刷レイアウト・コードの保守性が大幅に向上します。
この記事では、thead・tbody・tfoot それぞれの役割から、実務で使える高度なテクニック、よくあるミスとトラブルシューティングまで徹底解説します。
この記事で学べること
- thead・tbody・tfoot の HTML仕様と正しい使い方
- ブラウザの自動補完動作と省略時のリスク
- CSS で実現するヘッダー固定・tbody スクロール・印刷改ページ
- JavaScript での動的テーブル操作(行追加・ソート・フィルタ)
- アクセシビリティ対応(スクリーンリーダー・ARIA属性)
- レスポンシブテーブルの実装パターン
- 実務で頻出するミスと対処法
thead・tbody・tfootの全体像
テーブルのデータは一般的に、見出し(ヘッダー)・本体データ(ボディ)・集計・補足(フッター)という3つの性質に分かれます。thead・tbody・tfoot はそれぞれを論理的に分離するための要素です。
| 要素 |
役割 |
必須/任意 |
出現回数 |
<thead> |
列見出しのグループ化 |
任意 |
最大1個 |
<tbody> |
本体データのグループ化 |
任意(省略時は自動補完) |
1個以上 |
<tfoot> |
集計・補足情報のグループ化 |
任意 |
最大1個 |
重要なのは、これらが見た目のためではなく意味構造のために存在する点です。ブラウザ・スクリーンリーダー・検索エンジンは、これらの要素を認識してテーブルの構造を理解します。
基本構造
thead・tbody・tfoot をすべて使った基本的なテーブル構造を見てみましょう。
HTML – テーブルの基本構造
<table>
<thead>
<tr>
<th>商品名</th>
<th>単価</th>
<th>数量</th>
<th>小計</th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="3">合計</th>
<td>15,000円</td>
</tr>
</tfoot>
<tbody>
<tr>
<td>ノートPC</td>
<td>5,000円</td>
<td>2</td>
<td>10,000円</td>
</tr>
<tr>
<td>マウス</td>
<td>2,500円</td>
<td>2</td>
<td>5,000円</td>
</tr>
</tbody>
</table>
注意:上記のコードでは <tfoot> が <tbody> より前に記述されていますが、ブラウザは thead → tbody → tfoot の順に描画します。HTML5 では tfoot を tbody の後に書くことも許可されています。
HTML仕様から見るthead・tbody・tfoot
HTML Living Standard(WHATWG)の仕様に基づいて、各要素の正確な定義と制約を確認しましょう。
コンテンツモデルの制約
<table> 要素の子要素として許可される順序は仕様で厳密に定められています。
| 項目 |
仕様上の制約 |
| table の子要素の順序 |
caption? → colgroup* → thead? → (tbody* | tr+) → tfoot? |
| thead の出現回数 |
0回または1回のみ |
| tbody の出現回数 |
0回以上(複数可) |
| tfoot の出現回数 |
0回または1回のみ |
| thead/tbody/tfootの子要素 |
0個以上の <tr> 要素 |
| tbody と tr の混在 |
tbody と直接の tr は混在不可 |
ポイント:<tbody> は複数配置できます。データを論理的なグループに分けたい場合(例:部署ごと、カテゴリごと)に活用できます。
省略タグのルール
HTML では、特定の条件下でタグの省略が許可されています。
| 要素 |
開始タグ |
終了タグ |
省略条件 |
<thead> |
省略不可 |
条件付き省略可 |
直後が tbody/tfoot の場合 |
<tbody> |
条件付き省略可 |
条件付き省略可 |
最初の子がtrで直前の終了タグが省略されていない場合 |
<tfoot> |
省略不可 |
条件付き省略可 |
table の最後の子の場合 |
thead要素の詳細解説
<thead> はテーブルのヘッダー行をグループ化する要素です。列の見出しを定義し、テーブル全体の内容を理解するための基準情報を提供します。
theadの基本的な使い方
thead 内には通常 <th>(テーブルヘッダーセル)を配置します。scope 属性で見出しの方向を明示できます。
HTML – thead と scope 属性
<table>
<thead>
<tr>
<th scope="col">社員番号</th>
<th scope="col">氏名</th>
<th scope="col">部署</th>
<th scope="col">入社年</th>
</tr>
</thead>
<tbody>
<tr>
<td>001</td>
<td>田中太郎</td>
<td>開発部</td>
<td>2020</td>
</tr>
</tbody>
</table>
複数行のヘッダー
thead には複数の <tr> を含めることが可能です。多段ヘッダーで大分類・小分類を表現するときに使います。
HTML – 複数行ヘッダー(colspan/rowspan)
<thead>
<!-- 1行目: 大分類 -->
<tr>
<th rowspan="2">商品</th>
<th colspan="2">上半期</th>
<th colspan="2">下半期</th>
</tr>
<!-- 2行目: 小分類 -->
<tr>
<th>売上</th>
<th>利益</th>
<th>売上</th>
<th>利益</th>
</tr>
</thead>
theadを使うべきケース
| ケース |
thead使用 |
理由 |
| データ一覧表 |
必須 |
列見出しがデータの意味を定義 |
| 比較表・料金表 |
必須 |
比較項目のラベルが必要 |
| キーバリュー型(2列) |
任意 |
行見出しが th で十分な場合あり |
| レイアウト目的テーブル |
不要 |
そもそも table を使うべきでない |
tbody要素の詳細解説
<tbody> はテーブルの主たるデータ行をグループ化する要素です。テーブルの「本文」にあたる部分を囲みます。
tbodyの自動補完
<tbody> を省略しても、ブラウザは DOM を構築する際に自動的に tbody 要素を挿入します。これは HTML パーサーの仕様による動作です。
HTML – tbody を省略した場合
/* HTMLソースコード */
<table>
<tr>
<td>データ1</td>
</tr>
</table>
/* ブラウザが構築するDOM */
<table>
<tbody> <!-- 自動挿入される -->
<tr>
<td>データ1</td>
</tr>
</tbody> <!-- 自動挿入される -->
</table>
注意:この自動補完により、table.children や table > tr といった CSS セレクタ・JavaScript の操作が意図どおりに動かないケースがあります。後述のトラブルシューティングで詳しく解説します。
複数のtbodyを使うパターン
<tbody> は1つのテーブル内に複数配置できます。データをカテゴリごとにグループ化したい場合に有効です。
HTML – 複数のtbody(部署別グループ化)
<table>
<thead>
<tr>
<th>氏名</th>
<th>役職</th>
</tr>
</thead>
<!-- 開発部 -->
<tbody class="dept-dev">
<tr><td>田中</td><td>リーダー</td></tr>
<tr><td>佐藤</td><td>メンバー</td></tr>
</tbody>
<!-- 営業部 -->
<tbody class="dept-sales">
<tr><td>鈴木</td><td>マネージャー</td></tr>
<tr><td>高橋</td><td>メンバー</td></tr>
</tbody>
</table>
複数の tbody を使うことで、CSS でグループ間にボーダーを入れる、JavaScript で特定グループだけ表示/非表示を切り替えるといった操作が容易になります。
tbodyを明示すべき理由
省略しても動作はしますが、以下の理由から明示的に書くことを推奨します。
- コードの意図が明確:ヘッダー/ボディ/フッターの境界が視覚的にわかる
- CSS セレクタの信頼性:
tbody tr セレクタが確実にデータ行だけにマッチ
- JavaScript 操作の安全性:
table.tBodies[0] で確実にボディを取得
- チーム開発での可読性:他の開発者がテーブル構造を即座に理解できる
tfoot要素の詳細解説
<tfoot> は、合計行・注釈・補足情報など、テーブル全体に関わるまとめ情報を表す要素です。「テーブルのフッター」として、データ全体を俯瞰する情報を提供します。
tfootの典型的な使用例
最も一般的な使い方は集計行(合計・平均・件数など)の表示です。
HTML – tfoot による集計行
<table>
<thead>
<tr>
<th>月</th>
<th>売上</th>
<th>経費</th>
<th>利益</th>
</tr>
</thead>
<tbody>
<tr><td>1月</td><td>500万</td><td>300万</td><td>200万</td></tr>
<tr><td>2月</td><td>600万</td><td>350万</td><td>250万</td></tr>
<tr><td>3月</td><td>700万</td><td>400万</td><td>300万</td></tr>
</tbody>
<tfoot>
<tr>
<th>合計</th>
<td>1,800万</td>
<td>1,050万</td>
<td>750万</td>
</tr>
<tr>
<th>平均</th>
<td>600万</td>
<td>350万</td>
<td>250万</td>
</tr>
</tfoot>
</table>
tfootのその他の活用方法
tfoot は集計だけでなく、以下のような情報にも使えます。
- 注釈行:「※ 金額は税抜きです」のような補足
- データソース:「出典:総務省統計局」のような出典情報
- ページネーション:テーブル下部のナビゲーション情報
- 操作ボタン行:「すべて選択」「一括削除」などの UI
tfootと印刷の関係
tfoot の重要な特徴の一つが印刷時の動作です。テーブルが複数ページにまたがる場合、ブラウザは各ページの末尾に tfoot の内容を繰り返し表示します(thead も同様に各ページの先頭に繰り返されます)。
ポイント:この印刷時の繰り返し動作は CSS の display: table-header-group / display: table-footer-group によるものです。ただし、ブラウザによってサポート状況が異なるため、印刷プレビューで必ず確認しましょう。
記述順と表示順のルール
thead・tbody・tfoot で多くの開発者が混乱するのが、HTMLソースの記述順と画面上の表示順が一致しない場合があるという点です。
HTML4時代の記述順
HTML4 の仕様では、<tfoot> は <tbody> よりも前に記述する必要がありました。
HTML4 – tfoot を tbody の前に記述
<!-- HTML4 時代の正式な順序 -->
<table>
<thead>...</thead> <!-- 1. ヘッダー -->
<tfoot>...</tfoot> <!-- 2. フッター(先に記述) -->
<tbody>...</tbody> <!-- 3. ボディ -->
</table>
この仕様の意図は、ブラウザが大量のデータ行を読み込む前にフッターの情報を取得できるようにすることでした。プログレッシブレンダリングの観点から設計された仕様です。
HTML5(Living Standard)の記述順
HTML5 以降(HTML Living Standard)では、<tfoot> を <tbody> の後ろに記述することも許可されています。
HTML5 – tfoot を tbody の後に記述(推奨)
<!-- HTML5 で推奨される自然な順序 -->
<table>
<thead>...</thead> <!-- 1. ヘッダー -->
<tbody>...</tbody> <!-- 2. ボディ -->
<tfoot>...</tfoot> <!-- 3. フッター(後に記述) -->
</table>
現在のブラウザはどちらの記述順でも正しく描画するため、可読性の高い thead → tbody → tfoot の順序で書くことを推奨します。
| 記述パターン |
仕様 |
表示順 |
推奨度 |
| thead → tfoot → tbody |
HTML4/HTML5 両対応 |
thead → tbody → tfoot |
互換性重視なら有効 |
| thead → tbody → tfoot |
HTML5 で有効 |
thead → tbody → tfoot |
推奨(直感的) |
ブラウザの自動補完動作とその影響
ブラウザのHTMLパーサーは、テーブル内の <tr> を見つけると、それを囲む <tbody> が存在しない場合に自動的に生成します。この動作は便利な反面、予期しないバグの原因にもなります。
自動補完の具体的なケース
JavaScript – 自動補完による予期しない動作
// HTMLソース上は tbody がない
// <table id="myTable"><tr><td>A</td></tr></table>
const table = document.getElementById('myTable');
// 直感的にはこれで行が取れそうだが...
console.log(table.children.length); // 1(tr ではなく tbody)
console.log(table.children[0].tagName); // "TBODY"
// 正しい取得方法
console.log(table.tBodies[0].rows.length); // 1
console.log(table.rows.length); // 1(rows は全行を返す)
CSSでの影響
CSS の子セレクタ(>)を使う場合も注意が必要です。
CSS – 子セレクタと自動補完の罠
/* これは効かない(tbody が自動挿入されるため) */
table > tr {
background: #f0f0f0;
}
/* 正しい書き方 */
table > tbody > tr {
background: #f0f0f0;
}
/* または子孫セレクタ(こちらは tbody の有無に関係なく効く) */
table tr {
background: #f0f0f0;
}
CSSスタイリングテクニック
thead・tbody・tfoot を正しくマークアップすることで、CSS でのスタイリングが格段に柔軟になります。ここでは実務で役立つスタイリングパターンを紹介します。
基本的なセクション別スタイリング
各セクションに対して異なるスタイルを適用する基本パターンです。
CSS – セクション別スタイリング
/* ヘッダー: ダーク背景 + 白文字 */
thead {
background: #1e293b;
color: #fff;
}
thead th {
padding: 12px 16px;
font-weight: 600;
text-align: left;
letter-spacing: 0.05em;
}
/* ボディ: 交互行の色分け */
tbody tr:nth-child(even) {
background: #f8fafc;
}
tbody tr:hover {
background: #e2e8f0;
transition: background 0.2s ease;
}
/* フッター: 強調表示 */
tfoot {
background: #f1f5f9;
font-weight: 700;
border-top: 2px solid #334155;
}
ヘッダー固定(position: sticky)
データ量の多いテーブルでスクロールする際、ヘッダーを固定表示するテクニックです。<thead> を使っているからこそ、シンプルに実装できます。
CSS – sticky ヘッダー
/* テーブルを囲むコンテナ */
.table-container {
max-height: 400px;
overflow-y: auto;
}
/* thead 内の th を固定 */
thead th {
position: sticky;
top: 0;
background: #1e293b;
color: #fff;
z-index: 10;
/* スクロール時にボーダーが消えないように */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
注意:position: sticky は thead 自体ではなく、th 要素に適用する必要があります。thead に sticky を指定しても、多くのブラウザでは正しく動作しません。また、親要素に overflow: hidden があると sticky が無効になります。
tbodyスクロール(tbody固定高さ)
テーブル自体の中で tbody だけをスクロールさせるパターンもあります。ただし、この方法はレイアウトの制約が多く注意が必要です。
CSS – tbody スクロール
table {
width: 100%;
border-collapse: collapse;
}
thead, tbody, tfoot {
display: block; /* テーブルレイアウトを解除 */
}
tbody {
height: 300px;
overflow-y: auto;
}
/* 列幅を固定で揃える */
thead tr, tbody tr, tfoot tr {
display: table;
width: 100%;
table-layout: fixed;
}
注意:display: block に切り替えるとテーブル本来のレイアウトが失われ、列幅の自動調整が効かなくなります。table-layout: fixed と固定幅で対応しますが、前述の sticky ヘッダー方式のほうが実務では使いやすいです。
グループ間ボーダー(複数tbodyの装飾)
複数の <tbody> を使ったテーブルで、グループの区切りを視覚的に表現できます。
CSS – tbody間のボーダー
/* tbody 同士の間にスペースと線を入れる */
tbody + tbody {
border-top: 3px solid #0284c7;
}
/* 各 tbody の最初の行にクラス名的な装飾 */
tbody:nth-of-type(odd) {
background: #fafafa;
}
tbody:nth-of-type(even) {
background: #f0f9ff;
}
tfootの装飾パターン
tfoot はテーブルのまとめ情報として、ボディとは異なるスタイルで強調するのが一般的です。
CSS – tfoot の装飾
tfoot {
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
font-weight: 700;
}
tfoot td, tfoot th {
padding: 12px 16px;
border-top: 2px solid #334155;
font-size: 1.05em;
}
JavaScriptでのテーブル操作
thead・tbody・tfoot を正しく使っていれば、JavaScript でのテーブル操作が直感的かつ安全になります。DOM API にはテーブル専用のプロパティとメソッドが用意されています。
テーブル専用のDOMプロパティ
| プロパティ/メソッド |
説明 |
戻り値 |
table.tHead |
thead 要素を取得 |
HTMLTableSectionElement | null |
table.tFoot |
tfoot 要素を取得 |
HTMLTableSectionElement | null |
table.tBodies |
tbody 要素のコレクション |
HTMLCollection |
table.rows |
全セクションの行を取得 |
HTMLCollection |
table.createTHead() |
thead を作成(既存なら返す) |
HTMLTableSectionElement |
table.createTFoot() |
tfoot を作成(既存なら返す) |
HTMLTableSectionElement |
table.createTBody() |
tbody を新規作成して追加 |
HTMLTableSectionElement |
table.deleteTHead() |
thead を削除 |
void |
section.insertRow() |
セクション内に行を挿入 |
HTMLTableRowElement |
section.deleteRow() |
セクション内の行を削除 |
void |
行の動的追加
tbody に行を追加する基本的なパターンです。
JavaScript – tbody への行追加
const table = document.getElementById('dataTable');
const tbody = table.tBodies[0];
// 方法1: insertRow() + insertCell()
const row = tbody.insertRow();
row.insertCell().textContent = '田中太郎';
row.insertCell().textContent = '開発部';
row.insertCell().textContent = '2020';
// 方法2: innerHTML(大量データの一括追加に高速)
tbody.innerHTML += '<tr><td>佐藤花子</td><td>営業部</td><td>2021</td></tr>';
// 方法3: DocumentFragment(大量追加のベストプラクティス)
const fragment = document.createDocumentFragment();
const data = [
['山田次郎', '総務部', '2019'],
['鈴木三郎', '経理部', '2022']
];
data.forEach(([name, dept, year]) => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${name}</td><td>${dept}</td><td>${year}</td>`;
fragment.appendChild(tr);
});
tbody.appendChild(fragment);
ポイント:insertRow() を table に対して呼ぶと、最後の tbody(なければ自動生成された tbody)に行が追加されます。意図したセクションに追加するには、必ず tbody.insertRow() のように対象セクションを明示しましょう。
テーブルのソート機能
thead のクリックでテーブルをソートする実装例です。thead・tbody を分離しているからこそ、ヘッダーへのイベント設定とデータ行の並べ替えを分けて扱えます。
JavaScript – クリックソート機能
function sortTable(table, colIndex, ascending = true) {
const tbody = table.tBodies[0];
const rows = Array.from(tbody.rows);
rows.sort((a, b) => {
const aText = a.cells[colIndex].textContent.trim();
const bText = b.cells[colIndex].textContent.trim();
// 数値として比較可能ならば数値比較
const aNum = parseFloat(aText);
const bNum = parseFloat(bText);
if (!isNaN(aNum) && !isNaN(bNum)) {
return ascending ? aNum - bNum : bNum - aNum;
}
// 文字列比較
return ascending
? aText.localeCompare(bText, 'ja')
: bText.localeCompare(aText, 'ja');
});
// ソート結果を tbody に反映
rows.forEach(row => tbody.appendChild(row));
}
// thead の th にクリックイベントを設定
const table = document.getElementById('sortableTable');
let ascending = true;
table.tHead.querySelectorAll('th').forEach((th, index) => {
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
sortTable(table, index, ascending);
ascending = !ascending;
});
});
フィルタ機能の実装
tbody の行だけを対象にフィルタリングする実装です。thead・tfoot は常に表示させたまま、データ行だけを表示/非表示にできます。
JavaScript – テーブルフィルタ
function filterTable(table, keyword) {
const tbody = table.tBodies[0];
const lowerKeyword = keyword.toLowerCase();
Array.from(tbody.rows).forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(lowerKeyword)
? '' : 'none';
});
}
// 検索入力フィールドと連携
document.getElementById('searchInput')
.addEventListener('input', (e) => {
filterTable(
document.getElementById('dataTable'),
e.target.value
);
});
tfoot の集計値を動的に更新
tbody のデータが変わったときに、tfoot の集計値を自動更新するパターンです。
JavaScript – tfoot 集計値の自動更新
function updateTotals(table) {
const tbody = table.tBodies[0];
const tfoot = table.tFoot;
if (!tfoot) return;
// 各列の合計を計算(2列目以降が数値と想定)
const colCount = tbody.rows[0]?.cells.length || 0;
const totals = new Array(colCount).fill(0);
Array.from(tbody.rows).forEach(row => {
Array.from(row.cells).forEach((cell, i) => {
const num = parseFloat(
cell.textContent.replace(/[^0-9.-]/g, '')
);
if (!isNaN(num)) totals[i] += num;
});
});
// tfoot のセルに反映
const footCells = tfoot.rows[0].cells;
totals.forEach((total, i) => {
if (i > 0 && footCells[i]) {
footCells[i].textContent =
total.toLocaleString() + '円';
}
});
}
アクセシビリティへの影響
thead・tbody・tfoot の正しい使用は、アクセシビリティに大きな影響を与えます。スクリーンリーダーのユーザーは、テーブルの構造情報を頼りにデータを理解するためです。
スクリーンリーダーの読み上げ動作
代表的なスクリーンリーダー(NVDA, JAWS, VoiceOver)は、thead/tbody/tfoot の構造を以下のように利用します。
| 動作 |
説明 |
| 列見出しの自動読み上げ |
tbody のセルに移動すると、thead 内の対応する th が列見出しとして読み上げられる |
| セクション間ナビゲーション |
thead/tbody/tfoot の境界をジャンプで移動可能 |
| テーブルサマリーの提供 |
行数・列数・ヘッダー情報をテーブル到達時に通知 |
| セル移動時のコンテキスト |
上下左右のセル移動時に、行見出し・列見出しを併せて読み上げ |
適切なマークアップの例
HTML – アクセシブルなテーブル
<!-- caption でテーブルの概要を提供 -->
<table aria-describedby="table-desc">
<caption>2025年度 四半期売上報告</caption>
<thead>
<tr>
<th scope="col">四半期</th>
<th scope="col">売上</th>
<th scope="col">前年比</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Q1</th>
<td>1,200万円</td>
<td>+15%</td>
</tr>
<tr>
<th scope="row">Q2</th>
<td>1,500万円</td>
<td>+22%</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">年間合計</th>
<td>2,700万円</td>
<td>+18%</td>
</tr>
</tfoot>
</table>
ARIA属性との組み合わせ
複雑なテーブルでは、ARIA属性を追加してアクセシビリティを強化できます。
| ARIA属性 |
用途 |
使用例 |
aria-sort |
ソート状態の通知 |
thead の th に設定(ascending/descending/none) |
aria-describedby |
テーブルの補足説明 |
table 要素に設定 |
aria-label |
テーブルの名前 |
caption がない場合に table に設定 |
aria-live |
動的更新の通知 |
tbody に設定してデータ変更を通知 |
HTML – ソート対応テーブルのARIA属性
<thead>
<tr>
<th
scope="col"
aria-sort="ascending"
role="columnheader"
tabindex="0"
>
氏名 ▲
</th>
<th
scope="col"
aria-sort="none"
role="columnheader"
tabindex="0"
>
部署
</th>
</tr>
</thead>
印刷時のthead・tfootの動作
テーブルが複数ページにわたって印刷される場合、ブラウザは各ページの先頭にtheadを繰り返し表示し、各ページの末尾にtfootを繰り返し表示します。これはPDFへの変換時にも有効です。
印刷用CSSの設定
CSS – 印刷用テーブルスタイル
@media print {
/* thead を各ページの先頭に繰り返し表示 */
thead {
display: table-header-group;
}
/* tfoot を各ページの末尾に繰り返し表示 */
tfoot {
display: table-footer-group;
}
/* tbody は通常のテーブル行グループとして表示 */
tbody {
display: table-row-group;
}
/* 行の途中での改ページを避ける */
tr {
page-break-inside: avoid;
}
/* 印刷時の背景色を有効化 */
thead th {
background: #1e293b;
color: #fff;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
| ブラウザ |
thead繰り返し |
tfoot繰り返し |
| Chrome / Edge |
対応 |
対応 |
| Firefox |
対応 |
対応 |
| Safari |
対応 |
部分的(バージョンにより不安定) |
レスポンシブテーブルの実装
スマートフォンなどの小さな画面でテーブルを表示する場合、thead・tbody の構造を活かしたレスポンシブ対応パターンがあります。
パターン1: 横スクロール方式
最もシンプルな方法で、テーブル構造を維持したままスクロールで表示します。
CSS – 横スクロールテーブル
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive table {
min-width: 600px;
}
パターン2: カード型レイアウト変換
モバイルでは thead を非表示にし、各行をカード型に変換するパターンです。data-label 属性を使って列見出しを表示します。
HTML – data-label 属性付きテーブル
<tbody>
<tr>
<td data-label="氏名">田中太郎</td>
<td data-label="部署">開発部</td>
<td data-label="入社年">2020</td>
</tr>
</tbody>
CSS – カード型レスポンシブ変換
@media (max-width: 768px) {
/* thead を非表示 */
thead {
display: none;
}
/* 各行をカードとして表示 */
tbody tr {
display: block;
margin-bottom: 1em;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.5em;
}
tbody td {
display: flex;
justify-content: space-between;
padding: 0.5em 0.75em;
border-bottom: 1px solid #f1f5f9;
}
/* data-label で列見出しを疑似要素として表示 */
tbody td::before {
content: attr(data-label);
font-weight: 700;
color: #334155;
}
/* tfoot もカード型に */
tfoot tr {
display: block;
background: #f1f5f9;
border-radius: 8px;
padding: 0.5em;
font-weight: 700;
}
}
ポイント:カード型レイアウトでは data-label 属性に列名を重複して記述する必要があります。JavaScript で自動設定する方法もありますが、thead が非表示であってもDOMには残るため、スクリーンリーダーは引き続きヘッダー情報を利用できます。
実務で使えるテーブル設計パターン
ここまでの知識を踏まえて、実務で頻出するテーブルパターンを紹介します。
パターン1: 請求書テーブル
thead で項目名、tbody で明細行、tfoot で合計・消費税・請求金額を表示する典型的なパターンです。
HTML – 請求書テーブル
<table class="invoice-table">
<caption>請求明細(2025年3月分)</caption>
<thead>
<tr>
<th scope="col">No.</th>
<th scope="col">品目</th>
<th scope="col">単価</th>
<th scope="col">数量</th>
<th scope="col">金額</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Webサイト制作</td>
<td>500,000</td>
<td>1</td>
<td>500,000</td>
</tr>
<tr>
<td>2</td>
<td>保守・運用(月額)</td>
<td>30,000</td>
<td>1</td>
<td>30,000</td>
</tr>
</tbody>
<tfoot>
<tr>
<th colspan="4" scope="row">小計</th>
<td>530,000</td>
</tr>
<tr>
<th colspan="4" scope="row">消費税(10%)</th>
<td>53,000</td>
</tr>
<tr>
<th colspan="4" scope="row">合計</th>
<td><strong>583,000</strong></td>
</tr>
</tfoot>
</table>
パターン2: 時間割テーブル(行見出し + 列見出し)
行と列の両方に見出しがあるテーブルでは、thead と tbody 内の th を組み合わせて使います。
HTML – 行見出し + 列見出しの組み合わせ
<table>
<thead>
<tr>
<th></th> <!-- 空セル -->
<th scope="col">月曜</th>
<th scope="col">火曜</th>
<th scope="col">水曜</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1限</th>
<td>数学</td>
<td>英語</td>
<td>国語</td>
</tr>
<tr>
<th scope="row">2限</th>
<td>理科</td>
<td>社会</td>
<td>音楽</td>
</tr>
</tbody>
</table>
パターン3: データグリッド(CRUD操作付き)
管理画面で使われるような、データの追加・編集・削除機能付きテーブルの構造です。
HTML – CRUDテーブル構造
<table id="userTable">
<thead>
<tr>
<th>
<input type="checkbox"
id="selectAll">
</th>
<th>ID</th>
<th>名前</th>
<th>メール</th>
<th>操作</th>
</tr>
</thead>
<tbody aria-live="polite">
<!-- JavaScript で動的に行を追加 -->
</tbody>
<tfoot>
<tr>
<td colspan="5">
<button id="deleteSelected">
選択項目を削除
</button>
<span id="rowCount">
0件表示中
</span>
</td>
</tr>
</tfoot>
</table>
よくあるミスとトラブルシューティング
thead・tbody・tfoot に関連して、実務で遭遇しやすい問題とその解決策をまとめます。
ミス1: table > tr セレクタが効かない
| 問題 |
原因 |
解決策 |
table > tr で行にスタイルが当たらない |
ブラウザが自動で tbody を挿入するため、tr は table の直接の子ではない |
table > tbody > tr または table tr を使う |
ミス2: JavaScriptで行が取得できない
JavaScript – 誤った行取得と正しい行取得
// 誤り: table.children で直接 tr を期待
const table = document.querySelector('table');
// table.children[0] は tbody であり tr ではない
// 正解1: table.rows を使う(全セクションの行)
const allRows = table.rows;
// 正解2: tbody を明示的に取得
const dataRows = table.tBodies[0].rows;
// 正解3: querySelectorAll
const rows = table.querySelectorAll('tbody tr');
ミス3: thead/tfoot にtdを使ってしまう
thead 内のセルは <th> を使うべきです。<td> を使っても表示上は問題ありませんが、セマンティクスとアクセシビリティが損なわれます。
HTML – 誤った例と正しい例
<!-- 誤り: thead 内で td を使用 -->
<thead>
<tr>
<td>名前</td> <!-- th を使うべき -->
<td>年齢</td> <!-- th を使うべき -->
</tr>
</thead>
<!-- 正しい: thead 内で th を使用 -->
<thead>
<tr>
<th scope="col">名前</th>
<th scope="col">年齢</th>
</tr>
</thead>
ミス4: stickyヘッダーが効かない
| 原因 |
解決策 |
| thead に sticky を指定している |
thead th に指定する |
親要素に overflow: hidden がある |
親の overflow を見直す |
| スクロールコンテナが設定されていない |
max-height + overflow-y: auto のラッパーを追加 |
| 背景色が未指定で下の行が透けて見える |
th に background を必ず指定 |
ミス5: 複数tbodyで :nth-child が期待どおりに動かない
複数の tbody を使っている場合、:nth-child はそれぞれの tbody 内でリセットされます。
CSS – :nth-child と複数 tbody
/*
* 複数 tbody がある場合:
* tbody:first-of-type → 1つ目の tbody
* tbody:nth-of-type(2) → 2つ目の tbody
*
* 各 tbody 内の :nth-child は
* その tbody 内で独立してカウントされる
*/
/* 1つ目の tbody の奇数行 */
tbody:first-of-type tr:nth-child(odd) {
background: #eff6ff;
}
/* 2つ目の tbody の奇数行 */
tbody:nth-of-type(2) tr:nth-child(odd) {
background: #f0fdf4;
}
よくある質問(FAQ)
Q1: thead・tbody・tfoot は必須ですか?
仕様上は任意です。tbody は省略してもブラウザが自動補完します。ただし、明示的に記述することで可読性・保守性・アクセシビリティが向上するため、実務では必ず記述することを推奨します。
Q2: theadを使わずにthだけ使えばよいのでは?
<th> は個々のセルの見出し情報を伝えますが、<thead> は「この行グループ全体がヘッダーである」という構造的な意味を提供します。スクリーンリーダーのナビゲーション、印刷時の繰り返し、CSS のセクション別スタイリングなど、thead があることで初めて使える機能が多くあります。
Q3: tfootはいつ使うべきですか?
合計・平均・件数などの集計情報がある場合、または注釈・出典情報がある場合に使います。すべてのテーブルに必要なわけではなく、データの要約が必要な場合にのみ使えば十分です。
Q4: tbodyは複数使えますか?
はい、複数の tbody が許可されています。部署ごと・カテゴリごとなど、データを論理的にグループ化したい場合に複数 tbody を使うことで、CSS のスタイリングや JavaScript でのグループ操作が容易になります。
Q5: tfoot を tbody の前に書くのは正しいですか?
HTML4 では tfoot を tbody の前に書くことが必須でした。HTML5(Living Standard)ではどちらの順序も許可されています。現在では可読性の観点から thead → tbody → tfoot の順で書くことが推奨されます。
Q6: テーブルをレイアウト目的で使ってもよいですか?
使うべきではありません。テーブルはデータを表形式で表現するためのものです。レイアウトには CSS Grid や Flexbox を使いましょう。レイアウト目的のテーブルでは thead・tbody・tfoot も不要です。
実装チェックリスト
テーブルを実装する際に確認すべき項目をまとめました。
| カテゴリ |
チェック項目 |
| 構造 |
列見出しがある場合 thead を使用している |
| 構造 |
tbody を明示的に記述している |
| 構造 |
集計行がある場合 tfoot を使用している |
| 構造 |
thead 内のセルが th になっている(td ではない) |
| アクセシビリティ |
th に scope 属性(col/row)を付与している |
| アクセシビリティ |
caption または aria-label でテーブルの目的を説明している |
| CSS |
sticky ヘッダーは th に適用している(thead ではなく) |
| CSS |
table > tr ではなく table > tbody > tr のセレクタを使用 |
| JS |
行操作は table.tBodies[0] を経由している |
| 印刷 |
@media print で display: table-header-group を設定 |
| レスポンシブ |
横スクロールまたはカード変換の対応を実装している |
よくある質問(FAQ)
Q. thead・tbody・tfootはHTMLのテーブルで省略できますか?
A. tbodyは省略可能(ブラウザが自動補完)ですが、明示的に書くことで構造が明確になりCSS・JavaScriptの操作もしやすくなります。theadとtfootも同様に明示的に書くことを推奨します。
Q. tfootを使う実用的なシーンはどのような場合ですか?
A. 集計行(合計・小計)をテーブルの最下部に固定表示したい場合に適しています。また印刷時にtfootを各ページの末尾に繰り返し表示するブラウザもあります。
Q. theadのthをposition: stickyで固定する際の注意点は?
A. table要素のwidth: 100%とtable-layout: fixedを設定し、overflowを持つ親要素内でスクロールさせる構成が必要です。またborder-collapse: collapse;との組み合わせでボーダーが崩れる場合はborder-collapse: separate;に変更します。
まとめ
thead・tbody・tfoot は、テーブルを論理的に分割するための構造要素です。省略可能だからといって軽視すべきではありません。
この記事のポイント
- thead は列見出しのグループ。
<th scope="col"> と組み合わせてアクセシビリティを向上
- tbody は本体データのグループ。省略してもブラウザが自動補完するが、明示記述を推奨
- tfoot は集計・補足のグループ。印刷時に各ページ末尾で繰り返し表示される
- CSS では
position: sticky を th に適用してヘッダー固定、tbody のスタイル分離が可能
- JavaScript では
table.tHead・table.tBodies・table.tFoot で安全にアクセス
- アクセシビリティ では、スクリーンリーダーが thead の th を列見出しとして利用
- 印刷 では、thead/tfoot がページをまたいで繰り返し表示される
- レスポンシブ では、thead を非表示にして data-label で列名を表示するカード型変換が有効
テーブルは「データを並べて表示する」だけの要素ではなく、意味を持った構造として設計することが、保守性が高く、アクセシブルで、破綻しないHTMLにつながります。thead・tbody・tfoot を正しく使い分けることは、プロフェッショナルなWeb開発者としての基本スキルです。
まずは自分が作成しているテーブルに thead・tbody が正しく使われているか確認し、必要に応じて tfoot を追加するところから始めてみてください。小さな改善が、コード全体の品質を大きく向上させます。