はじめに
自分で作っている早見表のページを、幅375pxのスマホ表示で見たところ、表の上のタイトルが途中で切れていた。「同じ木(3ベット」で行が終わり、続きが読めない。表は横にスクロールできる箱(overflow-x:auto)に入れてあり、列が多いので、表そのものが画面より広い。
タイトルは <caption> に書いてあった。1回目は caption に display:block と max-width を当てて直そうとした。タイトルは収まったが、今度は表の組み方が崩れた。
そこで、直し方を7通り並べて、4つの画面幅(320・375・768・1280px)で、Chrome 156 で測り直した。結果、安全に直せたのは3通りだけだった。そのうち1通りは「100vw」を使う式で、本文の列が画面幅より狭い場所に表を置くと外れることも、別の測定で分かった。
この記事で分かること
-
<caption>は表と同じ幅で折り返される。表が箱より広いと、タイトルも箱より広い幅で組まれ、右端が画面外に出る - 切れるのは、表が箱より広くなる320pxと375pxだけ。768pxと1280pxでは同じ表で切れなかった
-
caption{display:block; max-width:…}は、375pxで caption が64px幅・12行になり、表の高さが217pxから455pxになった -
position:stickyにdisplay:blockとwidthを足す方法は、1280pxで表の幅が646pxから1,831pxに広がった -
padding-right:calc(100% - 100vw + 40px)は、4幅とも収まり、表の幅も変わらなかった。ただし列が画面より狭い場所では外れた。100cqwに変えると収まった - 測ったのは Chrome 156 だけ。Firefox と Safari は未測定
目次
測定のやり方
実測した環境
| 項目 | 内容 |
|---|---|
| ブラウザ | Google Chrome 156.0.8078.47(--headless=new) |
| OS | macOS 26.5 |
| 画面幅 | iframe の幅で 320・375・768・1280px を再現 |
| 表 | 列9本、white-space:nowrap、列の合計幅は646px |
| 箱 |
overflow-x:auto、画面幅から32px(左右に16pxずつ)引いた幅 |
| 測定日 | 2026年10月23日 |
| 未測定 | Firefox、Safari、実機のスマホ、スクロールバーの出る環境 |
測ったのは「画面の幅を変えたときの、caption の文字の右端と、表の幅」だ。表示のきれいさは測っていない。
測定の手順
ヘッドレスの Chrome は、--window-size に500px未満を渡しても、実際の幅が500px付近で止まる。375pxで測るには、iframe の幅を375pxにして、その中のページに測らせる必要があった。iframe の中では、100vw も iframe の幅になる。
- 測定ページ(
cap.html)が、同じ表を、直し方ごとに1つずつ作る - ページが読み込まれたら、caption の文字の範囲(
Range)の右端、表の幅、箱の右端を取って、<pre id="res">に JSON で書く - 外側のページ(
host.html)が iframe の幅を変えて読み込み、結果を--dump-domで取り出す
「切れた」の判定は、文字の範囲の右端が、箱の右端より1px以上外にあることにした。caption 要素の箱ではなく、文字の右端を見ているのは、直し方によっては箱が表と同じ幅のまま、文字だけが手前で折り返されるからだ。最初は caption の箱の右端で判定し、後述の式で「切れている」と誤判定した。
症状と仕組み
再現した症状
画面幅375pxで、何もしない版(A)の文字の右端は、実測で638pxだった。箱の右端は359px(左の余白16px+箱の幅343px)なので、279pxぶんが画面の外にある。
仕組み
<caption> は、表の一部として組まれる。CSS 2.2 の表の章では、caption の箱は表の外側の箱の中に置かれる。測った限り、caption の折り返しの幅は、列の合計である表の幅だった。表が箱より広いと、caption の文字も箱より広い幅で組まれ、右端が箱の外に出る。
画面幅が箱より広い768pxでは、同じ表と同じCSSでも、文字の右端は638px、箱の右端は752pxで、収まった。
PCで見て「問題なし」にした表が、スマホでだけ切れるのは、この条件による。画面を縮めて見ない限り、気づきにくい症状だ。
何もしない版は、どの幅で切れるか
320pxと375pxで切れ、768pxと1280pxでは収まった。表の幅は646pxで変わらないので、箱が646pxより狭くなる画面幅で起きる。
横スクロールする表自体は、WCAG 2.2 の Reflow(リフロー)でも、二次元のレイアウトが必要なものとして例外にされている。切れていいのは表の中身だけで、タイトルまで切れていいことにはならない。
直し方を試す
1回目の直し:display:block
最初に書いたのは次のCSSだった。
caption { display: block; max-width: calc(100vw - 32px); }
375pxでは、文字の右端が80pxに収まった。ただし caption は64px幅になり、12行に折れた。表全体の高さは217pxから455pxに伸びた。
広い画面では別のことが起きた。1280pxでは、表の幅が646pxから1,233pxに広がった(768pxでは736px)。display を変えると、caption が表の列幅の計算に混ざるらしい。ここまでは測定で、理由は測っていない。
sticky で固定する
タイトルを左に固定して、スクロールしても見えるようにする方法も試した。
/* G: sticky だけ */
caption { position: sticky; left: 0; }
/* H: block + sticky + width */
caption { display: block; position: sticky; left: 0; width: calc(100vw - 32px); box-sizing: border-box; }
G は、何も変わらなかった。320pxと375pxで切れたままで、表の幅も646pxのままだった。caption の箱が表と同じ幅なので、動ける余地がないのかもしれない(これは推測で、測っていない)。
H は文字が収まった。代わりに、表の幅が広がった。375pxで926px、1280pxで1,831pxになった。右に空の領域ができ、横スクロールの長さだけが増える。
padding-right で折り返させる
display を触らず、caption の右側に余白を足して、文字をそこで折り返させる方法を試した。
caption {
box-sizing: border-box;
padding-right: calc(100% - 100vw + 40px);
}
padding の % は、包含ブロックの幅(ここでは表の幅の646px)が基準になる。表が画面より広いときだけ、右に余白が付く。表が画面より狭ければ式は負になり、padding は0として扱われる。
結果は、実測で4幅とも「収まる」になり、表の幅は646pxのままだった(2026年10月23日の測定)。375pxでは、式の値から右余白は311pxになり、文字の右端は345px(箱の右端は359px)に収まった。
7通りの一覧
もう1つ、caption を <figure> の <figcaption> にして、表の外に出す方法(I)も測った。どの幅でも収まり、表の幅も変わらなかった。ただし表と caption が別の要素になるので、表の名前を支援技術に渡すには、aria-labelledby などを自分で書く必要がある。
もう1つ(J)は次の節で述べる。
100vwの式が外れる場所
C の式は 100vw(画面の幅)に頼っている。本文が画面いっぱいではなく、幅300pxの列の中に表を置くと、どうなるか。列を画面の左端から16pxの位置に置いて測った。
J は、画面の幅ではなく、横スクロールの箱自身の幅を測る。
.tw { container-type: inline-size; }
.tw caption {
box-sizing: border-box;
padding-right: calc(100% - 100cqw + 24px);
}
C は375px以上で切れた。J は4幅とも収まった。container-type:inline-size が要るので、箱の幅が内容で決まるレイアウトには向かない可能性がある(未測定)。
切れたタイトルを自動で見つける
直し方を決める前に、手元のページに同じ症状がいくつあるかを数える必要があった。目で全ページの全表を見るのは現実的でないので、「caption の文字の右端が、横スクロールの箱の右端を超えているか」をそのまま関数にした。
function clippedCaptions() {
const out = [];
for (const cap of document.querySelectorAll('caption')) {
let box = cap.closest('table').parentElement;
while (box && !/^(auto|scroll|hidden)$/.test(getComputedStyle(box).overflowX)) box = box.parentElement;
if (!box) continue;
const rg = document.createRange();
rg.selectNodeContents(cap);
const right = Math.max(...[...rg.getClientRects()].map(r => r.right));
const edge = box.getBoundingClientRect().right;
if (right > edge + 1) out.push({ text: cap.textContent.slice(0, 20), over: Math.round(right - edge) });
}
return out;
}
手順は3つで、どれも上の測定と同じ見方だ。caption から外側へ辿って、overflow-x が auto・scroll・hidden の最初の祖先を「箱」とする。caption の文字の範囲の右端を取る。箱の右端より1px以上外なら、切れているとして数える。
この関数を、測定ページ(A・B・C・G・H・J・N・M・I の各表を並べたもの)に入れて、4幅で数えた。期待する答えは、先の測定の textClipped から作った。
| 画面幅 | 関数が数えた件数 | 期待(測定の結果) |
|---|---|---|
| 320px | 2件 | A・G |
| 375px | 3件 | A・G・N |
| 768px | 1件 | N |
| 1280px | 1件 | N |
4幅とも、数も内訳も測定と一致した(実測)。N は、幅300pxの列に C の式を入れた表で、前の節の「切れる」例にあたる。切れた表を1件でも数えられたこと、直した版(C・J)を数えなかったことの両方が、この検出が使える最低限の根拠になる。
どの直し方を選ぶか
測った範囲で、選ぶ基準は次の3つ、避ける理由は1つだった。
-
本文が画面いっぱいに近く、列の幅が決まっているなら、
padding-rightの式(C)が1行で済む -
表が狭い列の中に入ることがあるなら、箱の幅を測る
100cqw版(J)にする。container-type:inline-sizeを箱に足す必要がある -
表の名前を別の要素で管理したいなら、
<figcaption>を外に出す(I)。ただし表の名前を支援技術へ渡す作業が自分に移る -
sticky だけ(G)は選ばない。何も変わらなかったので、直したつもりで残りやすい
display を変える方法(B・H)は、どちらも別の崩れを連れてきた。タイトルを直したくて display を触ると、表の幅の計算に影響が出た(実測)。
測定ページの全文
次の cap.html と host.html が、この記事の数字のすべてを出したものだ。%W% を幅に置き換えて、4回読み込んだ。
cap.html
<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{margin:0;padding:16px;font:16px/1.7 sans-serif}
.tw{overflow-x:auto;margin:0 0 24px}
table{border-collapse:collapse}
th,td{border:1px solid #999;padding:6px 10px;white-space:nowrap}
caption{text-align:left;font-size:14px;padding:4px 0}
.v-B caption{display:block;max-width:calc(100vw - 32px)}
.v-C caption{box-sizing:border-box;padding-right:calc(100% - 100vw + 40px)}
.v-J{container-type:inline-size}
.v-J caption{box-sizing:border-box;padding-right:calc(100% - 100cqw + 24px)}
.col{width:300px;margin:0 0 24px}
.v-G caption{position:sticky;left:0}
.v-N caption{box-sizing:border-box;padding-right:calc(100% - 100vw + 40px)}
.v-M{container-type:inline-size}
.v-M caption{box-sizing:border-box;padding-right:calc(100% - 100cqw + 24px)}
.v-H caption{display:block;position:sticky;left:0;width:calc(100vw - 32px);box-sizing:border-box}
</style></head><body>
<script>
const CAP='同じ木(3ベットポット)のターンで、コールが増える節と減る節を並べた表。右端の列に頻度が入る。';
const COLS=['節','盤面','行動','頻度A','頻度B','頻度C','頻度D','頻度E','頻度F'];
function tbl(cls,cap){
let h='<div class="tw v-'+cls+'" id="w'+cls+'"><table>'+(cap?'<caption>'+CAP+'</caption>':'')+'<tr>'+COLS.map(c=>'<th>'+c+'</th>').join('')+'</tr>';
for(let i=0;i<3;i++)h+='<tr>'+COLS.map((c,j)=>'<td>'+(j<3?'行'+i+c:(i+1)*11+'.'+j+'%')+'</td>').join('')+'</tr>';
return h+'</table></div>';
}
document.body.insertAdjacentHTML('beforeend',['A','B','C','G','H','J'].map(c=>tbl(c,true)).join('')+'<div class="col">'+tbl('N',true)+'</div><div class="col">'+tbl('M',true)+'</div>'+tbl('X',false)+'<figure style="margin:0" id="wI0"><figcaption id="capI" style="font-size:14px;padding:4px 0">'+CAP+'</figcaption>'+tbl('I',false)+'</figure>');
addEventListener('load',()=>{
const out={vw:innerWidth};
{const c=document.getElementById('capI'),w=document.getElementById('wI'),t=w.querySelector('table');const rg=document.createRange();rg.selectNodeContents(c);out.I={container:Math.round(w.getBoundingClientRect().width),table:Math.round(t.getBoundingClientRect().width),textRight:Math.round(Math.max(...[...rg.getClientRects()].map(x=>x.right))),capH:Math.round(c.getBoundingClientRect().height),textClipped:false,tableH:Math.round(t.getBoundingClientRect().height)};out.I.textClipped=out.I.textRight>w.getBoundingClientRect().right+1}
for(const id of ['A','B','C','G','H','J','N','M','X']){
const w=document.getElementById('w'+id),t=w.querySelector('table'),c=w.querySelector('caption');
const wr=w.getBoundingClientRect(),tr=t.getBoundingClientRect();
const r={container:Math.round(wr.width),table:Math.round(tr.width)};
if(c){const cr=c.getBoundingClientRect();r.capRight=Math.round(cr.right);r.capWidth=Math.round(cr.width);r.capH=Math.round(cr.height);
r.clipped=cr.right>wr.right+1;
// narrowest column
r.minCol=Math.min(...[...t.rows[0].cells].map(x=>Math.round(x.getBoundingClientRect().width)));
const rg=document.createRange();rg.selectNodeContents(c);r.textRight=Math.round(Math.max(...[...rg.getClientRects()].map(x=>x.right)));r.textClipped=r.textRight>wr.right+1;
r.capLines=new Set([...rg.getClientRects()].map(x=>Math.round(x.top))).size;}
r.tableH=Math.round(tr.height);
out[id]=r;
}
document.title='RESULT '+JSON.stringify(out);
const p=document.createElement('pre');p.id='res';p.textContent=JSON.stringify(out);document.body.appendChild(p);
});
</script></body></html>
host.html(%W% を 320・375・768・1280 に置き換える)
<!doctype html><meta charset="utf-8"><body style="margin:0"><iframe id="f" src="cap.html" style="border:0;height:900px" width="%W%"></iframe><pre id="res"></pre>
<script>document.getElementById('f').addEventListener('load',()=>{const d=document.getElementById('f').contentDocument;setTimeout(()=>{document.getElementById('res').textContent=d.getElementById('res').textContent},300)})</script>
実行は次のとおり。
CH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
sed "s/%W%/375/" host.html > host_375.html
"$CH" --headless=new --disable-gpu --allow-file-access-from-files \
--window-size=1400,1000 --virtual-time-budget=4000 \
--dump-dom file://$PWD/host_375.html
出力の <pre id="res"> に、各版の container(箱の幅)、table(表の幅)、textRight(文字の右端)、textClipped(切れたか)が JSON で入る。
測定の振り返り
測定で直した2つの誤りと1つの見落とし
この記事の数字は、最初から素直に出たわけではない。途中で2回、測り方そのものを直し、1つは見落としかけた点だ。
1つ目は、画面幅の作り方。 最初は --window-size=375,900 でページを開いた。ところが、ページ側で innerWidth を読むと、返ってきたのは500だった。ヘッドレスの Chrome は、この環境では幅500px未満の窓を作らなかった。375pxの結果として最初に出した数字は、実は500pxで測ったものだった。iframe の幅を指定する方式に変えて、innerWidth が375になることを確かめてから、すべて測り直した。
2つ目は、「切れた」の判定。 最初は caption 要素の箱の右端を、箱(横スクロールの領域)の右端と比べていた。この判定だと、padding-right の式(C)も「切れている」と出た。C は caption の箱を表と同じ幅のまま残し、中の文字だけを手前で折り返す。箱の右端は画面の外でも、文字は収まっていた。文字の範囲(Range)の右端で比べる方式に変えると、C は収まると出た。この方式を前の節の検出関数にも使っている。
3つ目は、幅の違い。 1280pxの結果だけを見ると、A も「収まる」になる。表が646pxで、箱が1,248pxあるので、当たり前だ。320pxと375pxを並べて初めて、条件つきで起きる症状だと確かめられる。PCだけで確認して終わらせる、というのは、この記事の症状そのものだった。
測らなかった直し方
ほかにも案はあったが、今回は測っていない。
-
white-space:nowrapを外して、表の列を折り返す。列が増えるほど縦に伸びるうえ、数値の列が折れて読みにくくなるので、早見表では選ばなかった - caption の文字を小さくする。375pxで収めるには、文字を646pxから343pxに近づける縮小が要り、読めなくなる
- caption を表の下(
caption-side:bottom)に置く。位置が変わるだけで、折り返しの幅は表の幅のままだと考えたが、測っていない
未測定の範囲
- Chrome 156 だけ。Firefox と Safari は測っていない。caption の幅の決まり方は、エンジンで違う可能性がある
-
iframe での再現。実機のスマホではない。スクロールバーが幅を取る環境では、
100vwがずれる - 表は1つ、タイトルは1文。列の数や文字の長さを変えた測定はしていない
-
日本語の折り返しは
word-breakの既定のまま。禁則やauto-phraseを足した場合は測っていない - 表は
white-space:nowrap。折り返す表では、そもそも切れないかもしれない
参考
- MDN: caption 要素 — 表のタイトル
- MDN: figcaption 要素 — I で使った
-
MDN: padding —
%の基準が包含ブロックの幅であること - MDN: 長さ(vw と cqw の単位) — ビューポート基準の単位
-
MDN: コンテナークエリー —
cqwの前提 -
MDN: container-type —
inline-size - MDN: overflow — 横スクロールの箱
-
MDN: position —
sticky - CSS 2.2: Tables — caption の箱の位置づけ
- WCAG 2.2: Reflow(理解のための文書) — 表が二次元レイアウトの例外であること
- W3C WAI: 表の caption と summary — 表の名前の与え方
- Headless Chrome(Chrome for Developers) — 測定の実行方法
同じ系統の測定記事
まとめ
-
<caption>は表の幅で折り返されるので、表が箱より広いと、タイトルの右端が箱の外に出る。切れたのは320pxと375pxだけだった -
display:block+max-widthは、375pxで caption が64px幅・12行になった。stickyにdisplay:block・widthを足す方法は、1280pxで表が1,831pxに広がった - 切れず、表の幅も変えなかったのは、
padding-right:calc(100% - 100vw + 40px)(C)、<figcaption>を表の外に出す方法(I)、100cqw版(J)の3つ - C は本文の列が画面より狭いと、375px以上で外れた。J は4幅とも収まった
- Chrome 156 だけの測定で、Firefox と Safari は未測定
「PCで見て問題なし」は、横スクロールする表では、タイトルの確認にならない。確認は、表が箱より広くなる幅で、文字の右端と箱の右端を比べるのが確実だった。
次の1アクション
手元の横スクロールする表のうち、<caption> を持つものを1つ選び、ブラウザの幅を375pxにして、上の clippedCaptions() をコンソールで試してみてほしい。1件以上返ったら、C か J を1行足して、同じ幅で見直すだけで済む。
公開しているサイトの表示崩れを、実機のブラウザで幅を変えて機械的に点検したい場合は、サイト監査の窓口に、対象のURLを書いて送ってほしい。







