表(テーブル)は、書けば書けるけれど「これで正しいのか」が分かりにくいマークアップです。行と列が揃って表示されていれば、目で見るぶんには問題なく読めます。ただ、画面を見ずに読み上げで内容をたどる人には、セルの値だけが並んで聞こえてくることになりかねません。そこを埋めるのが <caption> 要素と scope 属性です。この記事では、表に「名前」と「見出しとセルの対応づけ」を与える書き方を、動くコードを見ながら解説します。
目次
表は並べただけでは意味が伝わらない
目で表を読むとき、私たちは「この数字は、左端の『東京』の行で、上の『5月』の列だから、東京の5月の売上だ」と、縦横の位置関係から瞬時に判断しています。視線を上下左右に動かせることが前提の読み方です。一方、スクリーンリーダー(画面の内容を音声で読み上げるソフト)はセルを1つずつ順番に読んでいくので、手がかりがなければ「392」という数字だけが流れていきます。そこで支援技術は、表の構造から「このセルに対応する見出しはどれか」を割り出し、値と一緒に見出しも読み上げようとします。
つまり、表のアクセシビリティは「見出しセルとデータセルの対応づけが、マークアップから読み取れるか」でほぼ決まります。その対応づけを担当するのが scope 属性で、表そのものが何についての表なのかを示すのが <caption> 要素です。どちらも1つ足すだけで、見た目をほとんど変えずに情報量を増やせます。
なお、この記事では <thead> や <tbody>、colspan といった表の基本的な組み方には立ち入りません。そちらは別記事で扱っているので、ここでは「組み上がった表に意味を足す」ところに集中します。
caption 要素で表にタイトルを付ける
<caption> は、その表のキャプション(タイトル)を表す要素です。書き方は単純で、<table> の中にテキストを入れるだけです。
<table>
<caption>2026年度 第1四半期の売上(単位:万円)</caption>
<thead>
<tr>
<th>支店</th>
<th>4月</th>
<th>5月</th>
<th>6月</th>
</tr>
</thead>
<tbody>
<!-- 省略 -->
</tbody>
</table>
table の最初の子として書く
<caption> には、置ける場所がはっきり決まっています。MDN では、親である <table> 要素の最初の子要素として配置する必要があると説明されています。<thead> や <tbody> より後ろに書いたり、<tr> の中に入れたりはできません。<colgroup> を使っている表でも <caption> のほうが先です。間違えても表示上はブラウザーがだいたい何とかしてくれますが、その過程で要素が移動・調整されるため、意図したとおりの構造になっている保証はありません。
表の「名前」として扱われる
<caption> の中身は、単なる見出し風のテキストではありません。MDN の説明によれば、この要素は表のアクセシブル名(またはアクセシブル説明)を提供するものです。アクセシブル名というのは、支援技術がその要素を指すときに使う名前のことです。ボタンにとってのラベル、画像にとっての alt と同じ位置づけのものが、表にとっての <caption> になります。
スクリーンリーダーには、ページ内の表を一覧して移動する機能を持つものがあります。このとき手がかりになるのが表の名前です。名前がなければ「表」としか分からず、目的の表かどうかを判断するには中身を読み進めるしかありません。MDN も、キャプションがあることで表の残りの部分を読む必要があるか、飛ばすかを判断するのに役立つと述べています。
h2 見出しで代用するのとの違い
表の上に <h2>料金プランの比較</h2> と書いておけば、見た目としては同じようにタイトルが付きます。実際、そうしているページは珍しくありません。では <caption> を使う意味はどこにあるのでしょうか。
違いは「表と結び付いているかどうか」です。見出し要素は文書の階層を作るための要素で、あくまで「その位置から始まる区切り」を表します。直後にある表と機械的に結び付いてはいないので、表だけを取り出して読んだときにタイトルは付いてきません。表の一覧から直接その表に飛んだ場合も同じです。<caption> は <table> の中に書く要素なので、この結び付きが最初から保証されています。
併用しても構いません。ただし同じ文言を2回表示すると冗長なので、見出しをセクション名、キャプションを表の条件(集計期間や単位など)に振り分けると収まりが良くなります。なお、表が <figure> の中に入っていてその図の唯一の内容になっている場合は、MDN は <caption> ではなく <figcaption> を使うよう案内しています。
caption-side でキャプションを表の下に回す
<caption> は既定では表の上に表示されます。ただ、資料や論文のような体裁では「表1:〜」のような説明を表の下に置きたいこともあります。その位置を決めるのが CSS の caption-side プロパティです。
| 値 | 効果 |
|---|---|
top | 初期値。キャプションを表のブロック方向の先頭(通常は上)に配置する |
bottom | キャプションを表のブロック方向の末尾(通常は下)に配置する |
指定できるのは実質この2つです。仕様には inline-start と inline-end という論理値も定義されていますが、MDN によればどのブラウザーも対応していません。top と bottom のほうは2015年7月以降、すべての主要ブラウザーで利用できます。
実際に下に回してみます。「表1:〜」という説明を、表の下に左寄せで表示する例です。
<table>
<caption>表1:主要ブラウザーの対応状況(2026年3月時点の調査)</caption>
<thead>
<tr>
<th scope="col">ブラウザー</th>
<th scope="col">対応</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Chrome</th>
<td>対応</td>
</tr>
<tr>
<th scope="row">Firefox</th>
<td>対応</td>
</tr>
<tr>
<th scope="row">Safari</th>
<td>一部のみ</td>
</tr>
</tbody>
</table>
table {
border-collapse: collapse;
font-family: system-ui, sans-serif;
}
caption {
/* 表の下にキャプションを回す */
caption-side: bottom;
/* caption-side とは別に、文字の水平位置は text-align で決める */
text-align: left;
padding-top: 8px;
font-size: 0.875rem;
color: #64748b;
}
th,
td {
border: 1px solid #cbd5e1;
padding: 8px 16px;
text-align: left;
}
th {
background: #f1f5f9;
}
水平位置は text-align で別に指定する
上のデモで text-align: left を添えているのには理由があります。caption-side が決めるのは上下の位置だけで、左右の寄せ方には関与しません。多くのブラウザーの既定スタイルではキャプションは中央寄せになるので、左に揃えたいなら text-align で明示する必要があります。
ちなみに、かつては <caption align="bottom"> のように HTML の属性で位置を指定できました。この align 属性は現在は非推奨で、MDN も caption-side と text-align に置き換えるよう案内しています。
表の背景色はキャプションに及ばない
もう1つ、見落としやすい挙動があります。<table> に background-color を指定しても、キャプションには自動的には適用されません。表全体を色付きの箱のように見せたつもりが、キャプションだけ背景が抜けてしまいます。同じ色でつなげたいときは、caption にも同じ background-color を書いてください。
なお、caption-side: bottom で見た目を下に移しても、HTML 上は <caption> が <table> の最初の子であることに変わりはありません。表の名前としての役割も変わらないので、見た目の都合で位置を変えること自体は問題ありません。
scope 属性で見出しとセルの対応を示す
ここからが表のアクセシビリティの本体です。scope は <th>(見出しセル)に付ける属性で、その見出しがどの範囲のセルに関係しているかを示します。値は次の4つです。
| 値 | 意味 |
|---|---|
col | この見出しは、その列に属するすべてのセルに関連する |
row | この見出しは、その行に属するすべてのセルに関連する |
colgroup | この見出しは列グループに属し、その中のすべてのセルに関連する |
rowgroup | この見出しは行グループに属し、その中のすべてのセルに関連する |
いちばん使うのは col と row です。表の一番上の行に並ぶ見出しは列の見出しなので col、各行の左端に置く見出しは行の見出しなので row になります。
<table>
<caption>2026年度 第1四半期の売上(単位:万円)</caption>
<thead>
<tr>
<th scope="col">支店</th>
<th scope="col">4月</th> <!-- この列のセットの見出し -->
<th scope="col">5月</th>
<th scope="col">6月</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">東京</th> <!-- この行のセットの見出し -->
<td>410</td>
<td>392</td>
<td>455</td>
</tr>
</tbody>
</table>
これで「410」というセルには、列の見出しである「4月」と、行の見出しである「東京」の2つが対応づけられます。支援技術はこの対応を使って、セルの値だけでなく見出しも読み上げられるようになります。見た目はまったく変わらない属性なので、既存の表に後から足しても崩れる心配がない、というのも安心材料です。
省略したときに何が起きるか
scope は必須の属性ではありません。MDN の説明では、scope 属性が指定されていないか、その値が row / col / rowgroup / colgroup のいずれでもない場合、ブラウザーは自動的に見出しセルが適用されるセルの集合を選ぶとされています。つまり、書かなければ何も対応づかない、というわけではなく、位置から推測されます。
ではいつ書くべきか。MDN の使用上のメモには、単純な表では scope は推論されるため冗長になるが、一部の支援技術では正しく推論できない場合があるため、明示的に指定することで使い勝手が改善する可能性がある、という趣旨の説明があります。「書かないと壊れる」ではなく「書いておけば推論に頼らずに済む」という性質のものです。
推論が効きにくくなるのは、構造が単純でなくなったときです。見出しの行が2段ある、行の途中に小見出しの行が挟まる、colspan や rowspan でセルが結合されている、といった表では、どのセルがどの見出しに属するのかを位置だけから決めるのが難しくなります。単純な表ならあってもなくても結果は同じ、複雑な表ほど書く価値が上がると考えると判断しやすいでしょう。迷ったら書いておく方針で構いません。
rowgroup と colgroup でグループの見出しを示す
残る2つの値、rowgroup と colgroup は、1行や1列ではなくまとまり全体にかかる見出しに使います。
よくあるのは、比較表の途中に「基本機能」「サポート」のようなカテゴリー行を挟む形です。この行の見出しは、その1行だけでなく続く数行ぶんに効いています。こういうセルには scope="rowgroup" を付けます。範囲となる行グループは <tbody> で区切って表します。<tbody> は1つの表に複数書いてよいので、カテゴリーごとに分ければ「どこまでが1グループか」を構造として表現できます。
colgroup のほうは、見出しの行が2段になっていて、上段が「上半期」「下半期」のように複数列をまたいでいるときに使います。列のまとまりは <colgroup> 要素で表せますが、scope="colgroup" を付けるのはあくまで見出しセル(<th>)のほうです。
実際に、料金プランの比較表を組んでみます。列の見出しはプラン名、行の見出しは項目名、そしてカテゴリー行を scope="rowgroup" にした構成です。
<table>
<caption>料金プランの比較(税込・月額)</caption>
<thead>
<tr>
<td></td>
<th scope="col">フリー</th>
<th scope="col">スタンダード</th>
<th scope="col">ビジネス</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="rowgroup" colspan="4">基本機能</th>
</tr>
<tr>
<th scope="row">月額料金</th>
<td>0円</td>
<td>1,280円</td>
<td>4,800円</td>
</tr>
<tr>
<th scope="row">保存容量</th>
<td>1GB</td>
<td>50GB</td>
<td>500GB</td>
</tr>
<tr>
<th scope="row">メンバー数</th>
<td>1人</td>
<td>10人</td>
<td>無制限</td>
</tr>
</tbody>
<tbody>
<tr>
<th scope="rowgroup" colspan="4">サポート</th>
</tr>
<tr>
<th scope="row">メール対応</th>
<td>なし</td>
<td>あり</td>
<td>あり</td>
</tr>
<tr>
<th scope="row">電話対応</th>
<td>なし</td>
<td>なし</td>
<td>あり</td>
</tr>
</tbody>
</table>
table {
border-collapse: collapse;
font-family: system-ui, sans-serif;
font-size: 0.9375rem;
}
caption {
padding-bottom: 8px;
font-weight: bold;
text-align: left;
}
th,
td {
border: 1px solid #cbd5e1;
padding: 8px 14px;
text-align: center;
}
/* 列見出し(プラン名) */
thead th[scope="col"] {
background: #1e293b;
color: #fff;
}
/* 行グループの見出し */
th[scope="rowgroup"] {
background: #e2e8f0;
text-align: left;
}
/* 行見出し(項目名) */
tbody th[scope="row"] {
background: #f8fafc;
text-align: left;
font-weight: normal;
}
/* 左上の空セルは枠線を消しておく */
thead td:first-child {
border: none;
}
注目してほしいのは2点です。まず、左上のセルを <th> ではなく空の <td> にしています。行見出しの列と列見出しの行が交差するこのセルは、どちらの見出しでもないからです。カテゴリー行の <th scope="rowgroup"> に colspan="4" を付けているのは、列数を揃えないと表の格子が崩れるためです。
そして、グループごとに <tbody> を分けています。「基本機能」と「サポート」がそれぞれ別の <tbody> に入っているので、rowgroup がどこまでを指すのかが構造から読み取れます。すべてを1つの <tbody> にまとめてしまうと、この区切りが消えてしまいます。なお、このデモの CSS は th[scope="row"] のような属性セレクターでスタイルを当てています。scope を正しく書いておくと、余計なクラス名を足さずに見出しの種類ごとの指定ができる、という副次的な利点もあります。
scope で表せない表は headers と id で結ぶ
scope は「行」「列」「そのグループ」というきれいな範囲でしか対応づけを表せません。セルの結合が入り組んでいて、あるセルが位置だけでは決まらない複数の見出しに属する表では、表現しきれないことがあります。
そのときに使うのが headers 属性です。MDN の説明では、headers は空白区切りの一連の文字列で、そのセルの見出しを提供する <th> 要素の id 属性に対応するものです。<th> と <td> のどちらにも書けます。位置に頼らず、ID で「このセルの見出しはこれとこれ」と名指しするやり方です。
<table>
<caption>会議室の予約状況</caption>
<tr>
<td></td>
<th id="am" scope="col">午前</th>
<th id="pm" scope="col">午後</th>
</tr>
<tr>
<th id="roomA" scope="row">A会議室</th>
<td headers="roomA am">空き</td>
<td headers="roomA pm">予約済</td>
</tr>
<tr>
<th id="roomB" scope="row">B会議室</th>
<!-- 午前と午後が結合されたセル。見出しを2つとも名指しする -->
<td headers="roomB am pm" colspan="2">終日貸切</td>
</tr>
</table>
これなら、結合されたセルが「B会議室の、午前と午後」に属することを取り違えようがありません。ただし headers は、すべてのセルに手作業で ID を振って参照を書く必要があります。行や列が増えるたびに保守が要りますし、ID の書き間違いは見た目に現れないので気づきにくい、という弱点もあります。scope で表せる形なら scope で済ませるのが現実的です。そもそも headers なしでは表せないほど複雑な表は読む側の負担も大きいので、2つに分けられないか、行と列を入れ替えれば単純にならないかを先に検討してみてください。
読み上げが意図どおりにならないとき
属性は書いたのに期待した結果にならない、という場合、原因はだいたい次のどれかに当てはまります。
caption が table の最初の子になっていない
キャプションが表の名前として扱われるのは、<table> の最初の子要素として置かれているからです。テンプレートエンジンで表を組み立てていると、<thead> を出力したあとにキャプションを差し込んでしまう、といったことが起こります。<div> でキャプションを囲んでいるケースも、囲んだ時点で <table> の直接の子ではなくなるのでうまくいきません。
表示だけを見ても気づきにくいので、開発者ツールで出力された HTML を確認してください。<table> を開いたら次の行が <caption> になっているか、という単純なチェックで済みます。
見出しセルを td で作っている
これがいちばん多い原因です。デザイン上、見出しの行が太字でも中央寄せでもない場合、<th> の既定スタイルが邪魔に感じて <td> で組んでしまうことがあります。しかし <td> はデータセルなので、そこに scope を足しても見出しにはなりません。MDN も <td> に対する scope は非推奨としています。
見出しとして扱ってほしいセルは <th> で書き、見た目のほうを CSS で打ち消す。この順番を守るだけで解決します。<th> と <td> の選択は「太字にしたいかどうか」ではなく「そのセルが他のセルの見出しかどうか」で決める、と考えてください。
/* 行見出しを「ふつうのセル」に見せたいときは CSS で調整する */
tbody th[scope="row"] {
font-weight: normal; /* th の既定の太字を打ち消す */
text-align: left; /* th の既定の中央寄せを打ち消す */
}
scope の値を間違えている
scope は決まった値だけを取る属性です。MDN の説明にあるとおり、値が row / col / rowgroup / colgroup のいずれでもないときは、指定されていない場合と同じ扱い、つまりブラウザーの自動判定に戻ります。
やっかいなのは、値を間違えてもエラーにならず、見た目も変わらないことです。scope="column" や scope="rows" と書いてしまっていても、表は普通に表示されます。col と row は単数形、グループのほうは colgroup と rowgroup。ここだけ覚えておけば取り違えは防げます。心配なら HTML の検証ツールにかけると確実です。
display を変えて表の構造を壊している
横に長い表をスマートフォンでも見せたい、という要求から、table に display: block と overflow: auto を指定してスクロールさせる手法が知られています。手軽ですが副作用があります。MDN もこの方法について、この場合は表がその完全性の一部を失うと注意を添えています。表として扱われるための表示形式を、別のものに変えてしまうためです。レスポンシブ対応で tr を縦積みにしようと display: block を当てるパターンも同じで、見た目は整っても行と列の関係は失われます。
スクロールさせたいだけなら、<table> そのものではなく外側を囲む <div> に overflow-x: auto を指定するほうが安全です。表自体の表示形式は変わらないので、構造はそのまま残ります。
/* table ではなく、外側の div をスクロールさせる */
.table-scroll {
overflow-x: auto;
}
キャプションを display: none で隠している
キャプションを表示したくない場面もありますが、display: none や visibility: hidden を使うと、見た目と同時に支援技術からも内容が取り除かれ、<caption> を書いた意味がなくなります。視覚的にだけ隠したいときは、要素を画面外に追い出すなどの、いわゆる視覚的非表示のテクニックを使ってください。
レイアウト目的の表について
最後に一点だけ。ここまでの話は、すべて「表形式のデータを表として示している」ことが前提です。段組みのために <table> を使うと、存在しない行と列の関係を支援技術に伝えてしまいます。レイアウトは Flexbox や Grid の仕事です。すぐ組み直せない既存の表には、<table role="presentation"> と ARIA のロールを指定して表としての意味を取り除く手当てもありますが、あくまで応急処置です。
まとめ
<caption> は表のキャプション(タイトル)を示す要素で、<table> の最初の子要素として置きます。中身は表のアクセシブル名として扱われるため、直前に <h2> を置くのとは意味が違います。表示位置は既定で表の上、caption-side: bottom で下に回せます。左右の寄せ方は text-align の担当で、表に指定した背景色はキャプションには及びません。
scope は <th> に付けて、その見出しが行・列・行グループ・列グループのどれに効いているかを示す属性です。値は row / col / rowgroup / colgroup の4つ。指定しなければブラウザーが自動で判定しますが、単純でない表では推論が難しくなるため、明示しておくほうが確実です。それでも足りない複雑な表には、id と headers で見出しを名指しする方法があります。
どれも、属性を1つ書き足すだけの小さな作業です。見た目が変わらないぶん後回しにされがちですが、表を「読める」ものにする効果は確実にあります。次に表を書くときは、まず <caption> の1行から足してみてください。