記事に図を入れている。SVGでもPNGでも、作るときは「注記は15px、表は15px」のように数字を決めて書いていた。図を単体で開けば、ちゃんと読める。
ところが、公開したページの上でその文字を測ったら 6.95px だった。指定の半分以下になっていた。
原因は単純で、図は載せた面の幅に合わせて縮む。図の中に書いた font-size は指定値であって、表示値ではない。この2つを混同していた。しかも厄介なことに、縮小はエラーも警告も出さないし、はみ出しもしない。だから静的検査も目視も、まったく反応しない。
この記事は、その差を1つの式で管理できるようにするまでの記録。縮尺 = 表示幅 ÷ 設計幅、これだけ分かっていれば防げる。載せる面のカラム幅を Qiita / Zenn / note で実測した表と、実効サイズを全数出すコピペ用スニペットを置いておく。
目次
- 1. 何が起きていたか
- 2. 縮尺という1つの式
- 3. ラスタ画像の設計幅は naturalWidth ではない
- 4. 載せる面のカラム幅を実測する
- 5. 実効サイズを全数出すスニペット
- 6. SVG の直し方
- 7. ラスタ図の直し方
- 8. 検査に組み込む
- 9. まとめ
1. 何が起きていたか
1-1. 図は単体で開けば読める
はじめに書いておくと、これは「図の作りが雑だった」という話ではない。図のHTMLを開けば、注記も表もはっきり読める。ローカルで書き出したPNGを開いても読める。図そのものは、設計どおりに作れている。
問題は、その図を記事に貼って、記事を開いたときだけ起きる。画像は max-width:100% で表示されるので、キャンバス幅より狭いカラムに入れられた瞬間に縮む。1200pxで作った図を708pxのカラムに入れれば0.59倍になり、16pxで書いた注記は9.4pxで描かれる。
「作ったもの」と「配信されたもの」が違うので、作ったほうをいくら確認しても気づけない。
1-2. 静的検査は3本とも素通りした
記事を出す前に機械検査を通しているのだが、図の件についてはそのどれも反応しなかった。理由を並べると、見落としたのではなく構造的に見られないことが分かる。
| 検査 | 何を見ているか | 図の縮尺を見られるか |
|---|---|---|
| CSSのデザイン検査 | CSSの font-size 宣言 |
見られない(図はPNG、またはSVGの属性) |
| 記事の品質検査 |
<figure> の枚数 |
見られない(中身は数えていない) |
| SVGのはみ出し検査 |
getBBox で座標系の外に出た文字 |
見られない(縮小ははみ出さない) |
| 実描画で縮尺を測る |
getBoundingClientRect ÷ viewBox幅 |
これだけが見られる |
3本目がいちばん厄介だった。はみ出し検査は「文字が枠から出る」不具合には効く。だが縮小は枠の内側で起きる現象なので、getBBox の側から見ると完璧に正常に見える。「はみ出し0件」は「読める」の証明にならない。
1-3. 数字にすると
自分がこの面に出していた図は、740pxのキャンバスで作り、最小フォントを15pxにしていた。デスクトップのカラム幅708pxに対しては縮尺0.957なので、実効14.4px。ここは意図どおり。
問題は375pxの実機で、カラム幅は343pxまで落ちる。縮尺は 343 ÷ 740 = 0.464。15pxで書いた文字が6.95pxで描かれていた。本文の可読下限として自分が置いている14pxの、ちょうど半分だ。
2. 縮尺という1つの式
2-1. scale = 表示幅 ÷ 設計幅
覚えることは1つしかない。
縮尺 = 表示幅 ÷ 設計幅
実効px = 指定した font-size × 縮尺
「表示幅」は、そのページ上で実際に描かれている幅。getBoundingClientRect() の width で取れる。
問題は「設計幅」のほうで、ここを取り違えると答えが変わる。
2-2. SVGの設計幅は viewBox の幅
SVGの中に書いた座標や font-size は、viewBox が定義する座標系の中の値だ。viewBox="0 0 335 200" と書けば、その中の font-size="14" は「幅335の座標系における14」という意味であって、画面上の14pxではない。
実際に描かれる幅は、CSSで決まる。width:100% を当てれば親の幅になるし、width:335px と固定すれば335pxになる。そして preserveAspectRatio の既定値では、縦横比を保ったまま全体が均等にスケールされる。つまり文字だけ元のサイズで残る、ということは起きない。
// SVG の縮尺
const scale = svg.getBoundingClientRect().width / svg.viewBox.baseVal.width;
2-3. 「座標系の中で14px」は判定ではない
ここは自分が一度間違えたところなので、はっきり書いておく。SVGの中を検査して「font-size が全部14px以上ある」と確認しても、それは何も保証しない。座標系の中の話だからだ。
判定になるのは、縮尺を掛けたあとの値だけ。逆に言えば、縮尺が確定していない図は、どれだけ丁寧に作っても未検証の状態にある。
3. ラスタ画像の設計幅は naturalWidth ではない
3-1. 配信側でリサイズされる
ここが一番踏みやすい。ラスタ画像(PNG/JPEG)の場合、naturalWidth を分母にしたくなる。だがこれは配信されたファイルのピクセル数であって、自分が文字を置いた座標系の幅ではない。
Qiitaは投稿画像を imgix 経由で配信していて、URLに w=1400&fit=max が付く。fit=max は「指定サイズを超えるなら縮める、超えないなら触らない」という指定なので、1400pxより大きい画像は一律1400pxに丸められる。
3-2. どれくらいずれるか
自分の場合、740pxのキャンバスを --force-device-scale-factor=2 で書き出して1480pxのPNGにしていた。これが配信時に1400pxになる。ここで naturalWidth を分母にすると:
- 誤:
343 ÷ 1400 = 0.245 - 正:
343 ÷ 740 = 0.464
倍近くずれる。縮尺の分母は「配信されたピクセル数」ではなく、自分が文字を置いた座標系の幅。ラスタ画像の場合、この値はDOMのどこにも書いていないので、自分で覚えておくしかない。
なお currentSrc を見れば、実際に配信されたURL(=リサイズパラメータ付き)が確認できる。「なぜ naturalWidth が自分の書き出しと違うのか」を追うときはここを見るのが早い。同じ理由で、CDNやCMSを挟む面では書き出したファイルサイズをそのまま前提にしないほうがいい。
4. 載せる面のカラム幅を実測する
4-1. 測り方は1行
面のカラム幅は、ブラウザのコンソールで1行で取れる。1回測れば以後は使い回せるので、書く面ごとに1度だけやっておけばいい。
// 本文要素が分かっている場合(Qiita なら .it-MdContent)
document.querySelector('.it-MdContent').clientWidth
// セレクタが分からない面は、長い段落の実幅の最頻値を取る
(() => {
const m = {};
document.querySelectorAll('p').forEach(p => {
if (p.textContent.trim().length < 40) return;
const w = Math.round(p.getBoundingClientRect().width);
m[w] = (m[w] || 0) + 1;
});
return Object.entries(m).sort((a, b) => b[1] - a[1])[0]; // [幅, 出現数]
})()
最頻値を取っているのは、引用や表の中の段落が混ざっても答えがぶれないようにするため。本文の段落がいちばん数が多いので、素直に最頻値でいい。
4-2. 4つの面を実測した
自分が使う面を、デスクトップ(ビューポート1440px)と375pxの両方で測った。すべて2026-09-07の実測値。
| 面 | 1440px ビューポート | 375px ビューポート |
|---|---|---|
| Qiita | 708px | 343px |
| Zenn | 708px | 345px |
| note | 620px | 343px |
| 自分のサイト(参考) | 640px | 335px |
4-3. 708px は最大値であって固定値ではない
これは測っていて気づいたことなのだが、Qiitaのカラム幅は固定ではない。同じ記事をビューポート幅を変えて測ると:
| ビューポート幅 | 本文カラム幅 |
|---|---|
| 1920px | 708px |
| 1440px | 708px |
| 1280px | 668px |
| 691px | 595px |
708pxは上限で、ウィンドウが狭ければそれ以下になる。ノートPCを1280pxで使っている読者はそれなりにいるので、「708pxで割ればいい」と固定で覚えると、その層で計算が合わなくなる。安全側に倒すなら、狭いほうの数字で計算しておく。
4-4. 375px端末はどの面も343px前後だった
もうひとつ、表を見ると分かることがある。デスクトップのカラム幅は面によって88pxも違う(708 vs 620)のに、375px端末では3面とも343〜345pxにそろっている。375 − 左右16pxのパディング、という同じ設計に落ち着いているからだ。
これは実務上ありがたい。スマホ側だけを基準にすれば、Qiita / Zenn / note で同じ図を使い回せる。 面ごとに図を作り分ける必要はない。
5. 実効サイズを全数出すスニペット
5-1. SVG版
公開したページで走らせて、SVG内の文字を全数チェックする。目視で1枚見るのとは違って、縮尺という見えない量を数字にできるのが要点。
(() => {
const MIN = 14; // 実効サイズの下限(自分の基準)
const out = [];
document.querySelectorAll('svg').forEach((svg, i) => {
const vb = svg.viewBox && svg.viewBox.baseVal;
if (!vb || !vb.width) return; // viewBox が無いものは対象外
const scale = svg.getBoundingClientRect().width / vb.width;
svg.querySelectorAll('text, tspan').forEach(t => {
const spec = parseFloat(getComputedStyle(t).fontSize);
const eff = spec * scale;
if (eff < MIN) out.push({
svg: i,
scale: +scale.toFixed(3),
spec, eff: +eff.toFixed(2),
text: (t.textContent || '').trim().slice(0, 20)
});
});
});
console.table(out);
return `NG ${out.length} 件`;
})()
getComputedStyle を使っているのは、font-size がSVGの属性で書かれていてもCSSで書かれていても、同じように拾えるからだ。
5-2. ラスタ画像版
ラスタは前述のとおり naturalWidth が使えないので、設計幅を自分で渡す。
((designWidth, minSpec) => {
// designWidth: 図を作ったときのキャンバス幅(例: 740)
// minSpec: その図の中でいちばん小さい font-size(例: 15)
const rows = [];
document.querySelectorAll('img').forEach((img, i) => {
const w = img.getBoundingClientRect().width;
if (!w) return;
const scale = w / designWidth;
rows.push({
i,
表示幅: +w.toFixed(1),
縮尺: +scale.toFixed(3),
実効px: +(minSpec * scale).toFixed(2),
naturalWidth: img.naturalWidth, // 参考。分母には使わない
src: img.currentSrc.slice(-32)
});
});
console.table(rows);
})(740, 15)
5-3. 遅延読み込みを先に解除する
img は遅延読み込みされることが多いので、一度スクロールして全部読み込ませてから走らせる。読み込み前は getBoundingClientRect().width が0を返し、静かに全件スキップされる。0件と失敗が同じ見た目になるので、ここは必ず先にやる。
// 遅延読み込みを先に解除しておく
for (const img of document.querySelectorAll('img')) {
img.loading = 'eager';
img.scrollIntoView();
await new Promise(r => setTimeout(r, 300));
}
window.scrollTo(0, 0);
5-4. 375pxで走らせる
デスクトップで走らせても、いちばん厳しい条件は見えない。DevToolsのデバイスモードで375px幅にしてから実行する。devicePixelRatio は縮尺の計算には関係ない(CSSピクセル基準で計算する)ので、2にしても3にしても結果は変わらない。
ヘッドレスで回すなら、Chrome の headless モードに --window-size=375,812 を渡して同じスクリプトを流す。CIに載せるならこちら。ウェブフォントを使っている図は、読み込みが終わる前に測ると別のフォントで計算してしまうので、document.fonts.ready を待ってから実行する。
6. SVG の直し方
6-1. viewBox 幅をいちばん狭い表示幅に合わせる
SVGなら、この問題は完全に消せる。viewBox の幅を、その図が表示される最小幅に合わせればいい。
自分のサイトの記事に入れた図5点は viewBox="0 0 335 …"(375px端末のカラム幅335pxに合わせた)にしてある。実測すると:
| ビューポート | viewBox幅 | 表示幅 | 縮尺 | 最小の実効フォント |
|---|---|---|---|---|
| 375px | 335 | 335 | 1.000 | 14.00px(指定14pxがそのまま) |
| 1440px | 335 | 640 | 1.910 | 26.75px |
縮尺1.000なら、指定した数字がそのまま画面に出る。計算が要らなくなるので、これがいちばん楽だ。
6-2. 拡大は放っておいてよい
上の表のデスクトップ側は1.91倍に拡大されている。これは直さなくていい。
- 縮小 → 指定より小さく描かれる → 読めなくなる=害
- 拡大 → 指定より大きく描かれる → 読める=害がない
SVGはベクタなので拡大してもぼやけない。止めるのは縮小方向だけでいい、と決めると判断が速くなる。だから viewBox は広いほう(デスクトップ)ではなく、狭いほう(スマホ)に合わせる。逆にすると必ず縮む。
6-3. width:100% は親のパディングで効きが変わる
width:100% を当てたSVGは、親のパディング次第で想定より縮むことがある。カードの内側の figure の内側、のように入れ子が深いと、375pxの画面で実寸が270px前後まで落ちることがある。
実測したうえで、width:100%; min-width:<設計幅>px と親の overflow-x:auto を組み合わせると、狭い端末では図だけが横スクロールして本文は横スクロールしない、という形にできる。縮むよりは、図だけスクロールするほうがましだと判断した。
7. ラスタ図の直し方
7-1. キャンバス幅を面に合わせる
ラスタの場合も考え方は同じで、キャンバス幅を面のカラム幅に合わせる。書き出しは --force-device-scale-factor=2 などで2倍以上にしておけば、高解像度ディスプレイでもぼやけない。
この表から読めることが1つある。1200pxのキャンバスは、部分的には救えない。375px端末で下限14pxを満たそうとすると、設計時に49px以上で書く必要がある。見出し以外は全部これを下回るので、キャンバス幅を間違えた図は作り直しになる。だから4章のカラム幅の実測を最初にやる。
7-2. デスクトップとモバイルは両立しない
ラスタ図には、SVGと違って逃げ道がない。
- デスクトップ(708px)に合わせて740pxで作る → 375px端末で0.464倍
- 375px端末(343px)に合わせて343pxで作る → デスクトップで2.06倍に拡大される
後者は文字が読めなくなることはない(拡大なので)。ただし343px幅に置ける情報量は740pxよりずっと少ないので、表を3列にできない、横並びが組めないといった制約が出る。ラスタである以上、どちらを捨てるかを先に決めるしかない。
この記事の図は、前者(740px・最小15px)を選んでいる。デスクトップでは実効14.4pxで読め、スマホでは実効7pxになる。
7-3. 諦めた側で守ること
「スマホでは7px」と分かっているなら、図の作り方のほうを変える。
- 図に「読めないと意味が通らない細字」を置かない。注記・凡例・出典は本文に書く
- 図だけで完結させない。図で言っていることは本文にも書く。図はあくまで要約
- 数字や結論は、いちばん大きい文字(見出し・大きな数値)に載せる
スマホの読者は画像をタップして拡大できるが、拡大しないと意味が通らない図は、多くの人にとって存在しないのと同じになる。WCAG の Resize text は「200%まで拡大しても情報が失われないこと」を求めているが、逆に言えば拡大操作を前提にしないほうがいい。
8. 検査に組み込む
8-1. 書いた値を読むツールには構造的に見えない
一度こうやって整理すると、なぜ既存の検査が全部素通りしたのかがはっきりする。
書いた値を読むツールは、書いた値と描かれた値の差を持っていない。CSSをパースするツールにも、<figure> を数えるツールにも、getBBox を叩くツールにも、「この図がどの面に載って何倍になるか」という情報が最初から入っていない。設定を足せば直る類ではなく、入力に無いものは出力できないという話だ。
だから最終判定は実描画に置く。静的検査は「実描画をやる前のふるい」として使い、最後の合否には使わない。
8-2. 逆性能テストを先にやる
そのうえで、検査そのものを両方向で試す。
- わざと壊した入力で鳴ること(例:
viewBoxを1200にした図を混ぜて、NGが出るか) - 正しい入力で黙ること(例:
viewBox335の図で、NG 0件になるか)
2つ目を飛ばすと、適合しているものまで鳴る検査ができあがる。そして鳴りすぎる検査は必ず無視されるようになり、無視された検査は無い検査とまったく同じ働きしかしない。自分は別のところでこれをやって、自作の静的検査を1日に4回作り直している。
9. まとめ
- 図の中に書いた
font-sizeは指定値であって表示値ではない。縮尺 = 表示幅 ÷ 設計幅を1回計算する - 設計幅は、SVGなら
viewBoxの幅、ラスタなら制作キャンバスの幅。naturalWidthは配信側でリサイズされるので分母に使えない(Qiitaはw=1400&fit=maxで丸める) - 載せる面のカラム幅を先に実測する。Qiita 708/343、Zenn 708/345、note 620/343(デスクトップ/375px端末・2026-09-07実測)。708pxは上限で、1280pxのウィンドウでは668pxになる
- 375px端末はどの面も343px前後にそろっているので、スマホ基準で作れば図を使い回せる
- SVGなら
viewBox幅を最小表示幅に合わせて縮尺1.000にできる。拡大方向は害がないので放っておく - ラスタ図はデスクトップとモバイルを両立できない。どちらを捨てるかを先に決め、捨てたほうでは「細字がないと意味が通らない図」を作らない
- 静的検査には構造的に見えない。最終判定は実描画に置き、検査自体を「壊した入力で鳴るか」「正しい入力で黙るか」の両方向で試す
図を作るのは手間がかかる作業なので、作ったあとは「できた」で終わりにしがちだった。だが手間をかけて作った図ほど、読めていないときの損が大きい。
次の1アクション
次の1アクションはこれだけ。自分が最後に図を入れた記事を開いて、375px幅にして5-1(SVG)か5-2(ラスタ)のスニペットを1回試してみてほしい。 1分で終わるし、NG 0件ならそれで確定する。0件でなければ、この記事の4章に戻ってカラム幅を測るところからやり直せばいい。
この記事で実測に使ったページ:「AI導入に補助金は使える」と書いてあるものを、公募要領まで全部読んでみた(6章のSVG5点はこのページのもの)。図の作り方そのものについてはAIに書かせたHTML/CSSを168ファイル分測ってみたにも書いています。






