0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

表のタイトル(caption)がスマホで右端から切れた。直し方7通りを4つの画面幅で測ってみた

0
Posted at

はじめに

自分で作っている早見表のページを、幅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 の幅になる。

  1. 測定ページ(cap.html)が、同じ表を、直し方ごとに1つずつ作る
  2. ページが読み込まれたら、caption の文字の範囲(Range)の右端、表の幅、箱の右端を取って、<pre id="res"> に JSON で書く
  3. 外側のページ(host.html)が iframe の幅を変えて読み込み、結果を --dump-dom で取り出す

「切れた」の判定は、文字の範囲の右端が、箱の右端より1px以上外にあることにした。caption 要素の箱ではなく、文字の右端を見ているのは、直し方によっては箱が表と同じ幅のまま、文字だけが手前で折り返されるからだ。最初は caption の箱の右端で判定し、後述の式で「切れている」と誤判定した。

症状と仕組み

再現した症状

図1

画面幅375pxで、何もしない版(A)の文字の右端は、実測で638pxだった。箱の右端は359px(左の余白16px+箱の幅343px)なので、279pxぶんが画面の外にある。

仕組み

図2

<caption> は、表の一部として組まれる。CSS 2.2 の表の章では、caption の箱は表の外側の箱の中に置かれる。測った限り、caption の折り返しの幅は、列の合計である表の幅だった。表が箱より広いと、caption の文字も箱より広い幅で組まれ、右端が箱の外に出る。

画面幅が箱より広い768pxでは、同じ表と同じCSSでも、文字の右端は638px、箱の右端は752pxで、収まった。

PCで見て「問題なし」にした表が、スマホでだけ切れるのは、この条件による。画面を縮めて見ない限り、気づきにくい症状だ。

何もしない版は、どの幅で切れるか

図3

320pxと375pxで切れ、768pxと1280pxでは収まった。表の幅は646pxで変わらないので、箱が646pxより狭くなる画面幅で起きる。

横スクロールする表自体は、WCAG 2.2 の Reflow(リフロー)でも、二次元のレイアウトが必要なものとして例外にされている。切れていいのは表の中身だけで、タイトルまで切れていいことにはならない。

直し方を試す

1回目の直し:display:block

最初に書いたのは次のCSSだった。

caption { display: block; max-width: calc(100vw - 32px); }

図4

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 の箱が表と同じ幅なので、動ける余地がないのかもしれない(これは推測で、測っていない)。

図5

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)に収まった。

図6

7通りの一覧

図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);
}

図8

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つだった。

  1. 本文が画面いっぱいに近く、列の幅が決まっているなら、padding-right の式(C)が1行で済む

  2. 表が狭い列の中に入ることがあるなら、箱の幅を測る 100cqw 版(J)にする。container-type:inline-size を箱に足す必要がある

  3. 表の名前を別の要素で管理したいなら、<figcaption> を外に出す(I)。ただし表の名前を支援技術へ渡す作業が自分に移る

  4. 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。折り返す表では、そもそも切れないかもしれない

参考

同じ系統の測定記事

まとめ

  • <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を書いて送ってほしい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?