日本語の見出しが、単語の途中で折り返されていた。「そのままに」が「そのま/まに」になり、「確認」が「確/認」に割れる。スマホ幅で見出しを2行にすると、かなりの確率で起きる。
直そうとして text-wrap:balance を当てた。行の長さは揃ったが、割れる場所は変わらなかった。次に word-break:auto-phrase を当てた。何も起きなかった。 CSS.supports('word-break','auto-phrase') は true を返しているのに、折り返し位置は1文字も動かない。
原因は lang 属性だった。auto-phrase は「言語ごとの解析」を行う値なので、そのテキストが何語か分からなければ normal と同じ振る舞いに落ちる。そしてそれは静かに起きる。エラーも警告も出ないし、機能検出は true のままだ。
この記事は、その確認のために見出し6本を343px幅で5通りに折り返して、1文字ずつ矩形を取って測った記録。BudouX をブラウザに入れるときの落とし穴(import が通らない/通ると237KB落ちてくる)と、display:flex の中に文節の span を入れたら逆に1文字ずつ割れた話も込みで置いておく。
目次
- 1. 何が起きていたか
- 2. 測り方
- 3. text-wrap の balance は割れ方を直さない
- 4. auto-phrase は lang が無いと何もしない
- 5. 機能検出は「効くか」を答えていない
- 6. BudouX で文節境界を明示する
- 7. BudouX を CDN から import すると通らない
- 8. パーサとモデルだけなら11KBで足りる
- 9. flex の中に span を入れると逆に割れる
- 10. コピペで測るスニペット
- 11. まとめ
- 12. 参考
1. 何が起きていたか
1-1. 日本語には単語の区切りが無い
英語で text-wrap:balance が期待どおりに動くのは、単語の間に空白があるからだ。ブラウザは空白でしか折らないので、「行を均す」という操作をしても意味の切れ目は自動的に守られる。
日本語にはその空白が無い。UAX #14 (Unicode Line Breaking Algorithm) の既定では、漢字・ひらがな・カタカナはそれぞれが行頭にも行末にも来られる文字クラス(ID=Ideographic)に分類されていて、原則としてどの文字間でも折れる。禁則で弾かれるのは小書き仮名・長音符・約物といった一部だけだ。
だから日本語の折り返しは、放っておくと「入るところまで詰めて、入らなくなったところで折る」になる。文節の途中だろうが単語の途中だろうが関係ない。
1-2. 検査は通っていた
厄介だったのは、この状態で自前の改行検査が通っていたことだ。その検査が見ていたのは「行末に1〜2文字だけ孤立していないか」で、単語の途中で折れているかは見ていなかった。
孤立文字は消えていた。だから「合格」と出ていた。検査が見ていない欠陥は、存在しないことにされる。
自作の検査が当てにならなくなる理由は前に別記事でも書いたが、あのときは「鳴りすぎて無視される」話だった。今回は逆で、鳴るべきときに鳴っていない。
指摘を受けて最初にやるべきなのは直すことではなく、「その欠陥を見る検査」を足すことだった。今回で言えば「文節境界でない位置で行が変わった回数」を数える判定を追加することで、それがこの記事の測定の中身になっている。
1-3. 何を「文節境界」とみなすか
「単語の途中で折れた」を機械的に判定するには、正解の切れ目が要る。ここでは BudouX が返す文節列を正解に使った。BudouX は Google が公開している行分割用のセグメンタで、AdaBoost で「この文字間で切るか否か」を二値分類する。約15KBのモデルで動き、外部サービスを呼ばない。
判定は、行が変わった位置が
- BudouX の文節境界と一致している、または
- 直前の文字が約物(
、。・:;!?)」』】)である
のどちらかなら合格、それ以外は「文節外改行」として数えた。以降の表の「文節外改行 n / 6」はこの数え方による。
2. 測り方
2-1. 幅は343pxに固定した
測定幅は 343px にした。375px幅の端末で、左右16pxずつの余白を引いた値だ。Qiita・Zenn・note の本文カラムを375pxビューポートで測ると、いずれも343〜345pxに収まる。スマホでいちばん折り返しが厳しくなるのがこの幅なので、ここで測れば大きい幅は自動的に楽になる。
見出しは font-size:26px / line-height:1.4 / font-weight:700。日本語フォントを明示する(後述)。行間は折り返し位置には影響しないので(行ボックスの高さが変わるだけ)、1.5 でも同じ結果になることは実測で確認した。
.col { width: 343px; }
h2 {
font-size: 26px;
line-height: 1.4; /* 見出しの行間。折り返し位置には影響しない */
font-weight: 700;
font-family: "Hiragino Sans", "Hiragino Kaku Gothic ProN",
"Yu Gothic Medium", Meiryo, sans-serif;
}
2-2. 行の中身は1文字ずつ矩形を取って復元する
要素の textContent を見ても、どこで折れたかは分からない。innerText も改行位置までは返さない。実際に折れた位置を知るには、1文字ずつ Range を作って矩形を取り、top が同じものを1行にまとめるのがいちばん確実だった。
function lines(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
const nodes = [];
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
if (n.textContent) nodes.push(n);
}
const range = document.createRange();
const out = [];
for (const node of nodes) {
const t = node.textContent;
for (let i = 0; i < t.length; i++) {
const ch = t[i];
if (ch === '' || ch === '\n') continue; // ZWSP は幅0なので飛ばす
range.setStart(node, i);
range.setEnd(node, i + 1);
const b = range.getBoundingClientRect();
if (!b.width && !b.height) continue;
const line = out.find(l => Math.abs(l.top - b.top) < 4);
if (line) line.txt += ch;
else out.push({ top: b.top, txt: ch });
}
}
out.sort((a, b) => a.top - b.top);
return out.map(l => l.txt);
}
Math.abs(l.top - b.top) < 4 の許容は、上下に伸びる約物やルビで top が数px動くことがあるため。ここを厳密一致にすると1行が2行に割れて数えられる。
2-3. 環境
- Chrome 152.0.7977.77(
--headless=new) - 見出し6本(20〜23字)を5通りの指定で描画
- 測定日 2026-09-08
見出しは実際に自分のサイトで割れていたものと同じ長さ・同じ構造のものを、この記事用に書き下ろした。
3. text-wrap の balance は割れ方を直さない
3-1. 結果
まず text-wrap:balance を当てた場合。文節外改行は 5 / 6 だった。何も指定しない場合の 6 / 6 から、1本しか減っていない。
3-2. balance が均しているのは行の長さ
MDN の text-wrap-style は balance をこう定義している。各行の文字数が最もよく釣り合うように折り返す、と。つまり最適化の対象は行の長さであって、そこが意味の切れ目かどうかは評価関数に入っていない。
英語ではこれで問題が起きない。折れる候補が空白の位置しか無いので、「均す」だけで自動的に単語境界が守られるからだ。日本語では候補がほぼ全文字間にあるので、均した結果として単語の真ん中が選ばれる。
3-3. balance が割れ方を悪化させた例
さらに厄介なことに、balance が新しく割ったケースがあった。
| 指定 | 折れ方 |
|---|---|
| 何もしない | 本番環境に出す前に確認した / い3つのチェック |
text-wrap:balance |
本番環境に出す前に確 / 認したい3つのチェック |
何もしない状態では「確認」は割れていなかった。balance を当てたことで「確/認」という熟語の分断が生まれている。行の長さは確かに均されている(左が短くなった)ので、balance の仕事としては正しい。正しく動いた結果として、読みにくくなった。
3-4. 行数の上限がある
もう1つ実務上の注意として、balance は計算コストの都合で釣り合わせる行数に上限がある。MDN によれば Chromium は6行以下、Firefox は10行以下。それを超えるブロックでは効かない。見出し・キャプション・引用のような短いブロック向けの機能で、本文の段落に当てても無意味だ。
3-5. それでも balance を外す必要はない
以上は「balance は割れ方を直さない」という話であって、「balance が悪い」ではない。孤立文字(行末に1〜2文字だけ残る状態)を減らす効果は実際にある。後述の BudouX と併用しても副作用は出なかった(結果は同じ 0 / 6)。役割が違う機能を、同じ問題の解として期待していたのが間違いだった。
4. auto-phrase は lang が無いと何もしない
4-1. 宣言しても折り返しが1文字も動かない
次に word-break: auto-phrase を当てた。MDN の定義は「normal と同じだが、言語ごとの解析を行って自然な文節の途中で改行しないようにする」。まさに欲しかったものだ。
当てた。文節外改行は 6 / 6 のまま、1文字も動かなかった。
CSS.supports('word-break', 'auto-phrase') は true。DevTools の Styles でも取り消し線は付かず、getComputedStyle(el).wordBreak は "auto-phrase" を返す。「効いていない」ことを示すシグナルが、どこにも出ない。
4-2. lang 属性だけを変えて測り直した
CSSを固定したまま lang 属性だけを5パターン変えて測り直した結果がこれ。
| 言語の指定 | 文節外改行 | CSS.supports |
|---|---|---|
lang 指定なし |
6 / 6 | true |
<html lang="ja"> |
1 / 6 | true |
<html lang="en"> |
6 / 6 | true |
<h2 lang="ja"> |
1 / 6 | true |
<h2 lang="en"> |
6 / 6 | true |
要素側に付けても文書側に付けても同じで、ja かどうかだけが効いている。lang="en" を付けた日本語テキストは、lang が無いのと同じ扱いになった。
考えてみれば当然で、auto-phrase は「言語ごとの解析」をする値なので、言語が特定できなければ解析のしようがない。仕様どおりの挙動だ。ただしその事実は、機能検出からもDevToolsからも見えない。
5. 機能検出は「効くか」を答えていない
5-1. supports が答えているのは別の質問
ここが今回いちばん学びになった点だった。
CSS.supports() が答えているのは「このブラウザがこの値をパースできるか」であって、「今このページでその効果が出るか」ではない。auto-phrase のように、入力データ(この場合は言語情報)に依存する機能では両者が食い違う。
同種のものは他にもある。hyphens: auto も辞書が必要なので lang が無ければ効かない。font-feature-settings も、当てたフォントがその機能を持っていなければ黙って無視される。「サポートされている」と「効いている」は別の質問で、後者は結果を測るしかない。
5-2. auto-phrase の対応ブラウザ
auto-phrase 自体の対応状況も確認しておく(2026-09-08 時点、caniuse 参照)。
| ブラウザ | 対応 |
|---|---|
| Chrome / Edge | 119 以降 |
| Opera | 105 以降 |
| Firefox | 未対応 |
| Safari | Technology Preview のみ |
Firefox と Safari が入っていないので、全員に効かせたいなら CSS だけでは足りない。
5-3. とはいえ lang は先に直す
対応が半分でも、<html lang="ja"> を付ける作業は先にやる価値がある。1行で、JSも増えず、auto-phrase 以外にもスクリーンリーダーの読み上げ言語・フォントのフォールバック・翻訳の判定に効く。WCAG 3.1.1 Language of Page では達成基準(レベルA)そのものでもある。
5-4. ついでに見つかった、日本語フォントの指定漏れ
測定していて別件も出た。getComputedStyle(document.body).fontFamily が欧文専用フォント+sans-serif だけになっていて、日本語のグリフが端末任せのフォールバックで出ていた。Mac ではヒラギノ、Windows では游ゴシックと、環境ごとに字幅が変わる。
折り返し位置は字幅で決まるので、これを放置したまま「343pxで2行になる」を検証しても意味がない。font-family に日本語フォントを明示してから測り直した。
CSSの宣言と実際に描かれる値がずれるパターンは他にもあって、font-size の宣言を168ファイル分数えた話でも同じ構図に当たっている。
6. BudouX で文節境界を明示する
6-1. ZWSP を入れて keep-all で止める
ブラウザ任せにできないなら、切ってよい場所を自分で埋め込む。BudouX が推奨している方法がこれで、文節の境界にゼロ幅スペース(U+200B)を入れ、CSS で他の位置の改行を禁止する。
.budoux {
word-break: keep-all; /* CJK 文字間での改行を止める */
overflow-wrap: anywhere; /* ただし1文節が幅を超えたら折る */
}
keep-all は MDN の定義どおり「CJK テキストでは改行を使わない」。ZWSP は改行機会そのものなので keep-all の対象外で、そこだけが折れる。overflow-wrap: anywhere は保険で、文節1つが行幅を超えたときにはみ出さないようにする(この保険が発動したら、文言が長すぎるという合図でもある)。
6-2. 結果
文節外改行は 0 / 6。
| 指定 | 文節外改行 |
|---|---|
| 何もしない | 6 / 6 |
text-wrap:balance |
5 / 6 |
word-break:auto-phrase(lang なし) |
6 / 6 |
word-break:auto-phrase(lang="ja") |
1 / 6 |
BudouX + keep-all |
0 / 6 |
BudouX + keep-all + balance
|
0 / 6 |
auto-phrase が1本だけ残したケースも BudouX では消えた。両者は同じ系統の技術(auto-phrase の実装は BudouX と同じ発想の文節解析)だが、モデルとバージョンが違うので結果は完全一致しない。
6-3. 切るのは実行時でなくてよい
BudouX を「ブラウザに読み込むライブラリ」だと思っていたが、やっていることは文字列に ZWSP を挿入するだけなので、ビルド時に済ませられる。Node 側で切って HTML に埋め込めば、ブラウザに追加のJSは1バイトも要らない。
// build 時に実行する
import { Parser } from 'budoux/module/parser.js';
import { model as jaModel } from 'budoux/module/data/models/ja.js';
const parser = new Parser(jaModel);
const insertZWSP = (s) => parser.parse(s).join('');
console.log(parser.parse('設定ファイルの変更、そのままにしていませんか?'));
// [ '設定ファイルの', '変更、', 'そのままに', 'していませんか?' ]
言語モデルのファイルが export しているのは model という名前で、jaModel ではない。budoux のエントリポイントが import { model as jaModel } で読み替えているだけなので、モデルファイルを直接 import するときは { model } で受ける。
6-4. ZWSP を入れる対象を絞る
全文に入れる必要はない。折り返しが問題になるのは短い表示テキスト――見出し、ボタン、ブロックリンク、表のセル、図キャプション、サイドバーの項目――で、本文の段落は対象外でいい。長い段落の最終行が短くなるのは正常な組版であって、直す対象ではない。
対象を絞ると、埋め込む ZWSP の数も、後述の検査の対象も小さく保てる。
7. BudouX を CDN から import すると通らない
実行時に切りたい場合は、ブラウザに BudouX を読み込むことになる。ここで詰まったので記録しておく(すべて budoux 0.9.1 で 2026-09-08 に実測)。
7-1. module を直接 import すると解決に失敗する
いちばん素直に見える書き方は動かなかった。
// どちらも失敗する
import { loadDefaultJapaneseParser } from 'https://cdn.jsdelivr.net/npm/budoux@0.9.1/module/index.js';
import { HTMLProcessor } from 'https://cdn.jsdelivr.net/npm/budoux@0.9.1/module/html_processor.js';
TypeError: Failed to resolve module specifier "linkedom".
Relative references must start with either "/", "./", or "../".
html_processor.js が ./dom.js を読み、その dom.js が import { DOMParser } from 'linkedom' している。linkedom はサーバ側でDOMを作るためのパッケージなので、ブラウザには当然無い。
パッケージ側にはちゃんと対策があって、package.json の browser フィールドで ./module/dom.js → ./module/dom-browser.js に差し替えるよう指定されている。ただし browser フィールドはバンドラの規約であって、ブラウザのモジュール解決の仕様ではない。CDN から生の ESM を読むと当然無視される。
これは BudouX 固有の話ではなく、browser フィールドを持つあらゆる npm パッケージで起きる。「バンドラ経由なら動くのに、CDN 直読みだと落ちる」パッケージの正体はだいたいこれだ。
7-2. /+esm は動くが237KB落ちてくる
jsDelivr の /+esm を使うと import は通る。裸のモジュール指定子を CDN の URL に書き換えてくれるからだ。
import { loadDefaultJapaneseParser } from 'https://cdn.jsdelivr.net/npm/budoux@0.9.1/+esm';
// → 通る
ただし「書き換える」だけなので、linkedom も一緒に落ちてくる。performance.getEntriesByType('resource') で実際の転送量を測った。
| リクエスト数 | 21 |
転送バイト(encodedBodySize 合計) |
237,345 B |
展開後(decodedBodySize 合計) |
616,529 B |
内訳には linkedom、htmlparser2、css-select、cssom、dom-serializer が並び、HTMLエンティティ表の entities はバージョン違いで3つ(4.2.0 / 7.0.1 / 8.0.0)入っていた。日本語の見出しにゼロ幅スペースを入れるために、サーバ用のDOM実装一式をダウンロードしていることになる。
8. パーサとモデルだけなら11KBで足りる
8-1. DOM に触るクラスを使わなければ linkedom は要らない
Parser クラスは DOM に一切触らない。parse() と parseBoundaries() しか持っていないからだ。DOM を触るのは HTMLProcessor のほうで、そちらを使わなければ linkedom も要らない。
import { Parser } from 'https://cdn.jsdelivr.net/npm/budoux@0.9.1/module/parser.js';
import { model } from 'https://cdn.jsdelivr.net/npm/budoux@0.9.1/module/data/models/ja.js';
const parser = new Parser(model);
for (const el of document.querySelectorAll('h1,h2,h3,.card-title')) {
el.textContent = parser.parse(el.textContent).join('');
el.style.wordBreak = 'keep-all';
el.style.overflowWrap = 'anywhere';
}
| 読み込むもの | gzip サイズ | 実測の転送量 |
|---|---|---|
module/parser.js |
1,260 B | — |
module/data/models/ja.js |
9,630 B | — |
| 合計 | 10,890 B | 11,131 B |
上のコードはそのまま headless Chrome で走らせて確認した。リクエストは2本、encodedBodySize の合計は 11,131 B、見出しは「設定ファイルの変更、/そのままにしていませんか?」と文節で折れた。
2ファイル・11.1KB。/+esm の 237KB に対して 21.3分の1 になった。転送量はブラウザで実測した encodedBodySize の合計で、/+esm の数字と同じ測り方で並べてある。
textContent を書き換えているので、中に <b> などの要素を持つ見出しでは子要素が消える。要素構造を保ったまま入れたい場合は HTMLProcessor が必要で、その場合はバンドラを通すか /+esm を選ぶことになる。要素を持たない見出しなら、パーサだけで足りる。
8-2. カスタム要素だけでよければ1リクエスト
3つ目の道として、公式のバンドルを読み込んで <budoux-ja> で囲む方法がある。
<script src="https://cdn.jsdelivr.net/npm/budoux@0.9.1/bundle/budoux-ja.min.js"></script>
<budoux-ja>設定ファイルの変更、そのままにしていませんか?</budoux-ja>
1リクエスト・13,579 B。実際に <budoux-ja> の中身に ZWSP が入り、word-break:keep-all; overflow-wrap:anywhere が要素に付くところまで確認した。ただしこのバンドルは IIFE で、公開しているのはカスタム要素の登録だけ(export を持たないので import しても何も受け取れない)。既存のマークアップを書き換えられるならこれがいちばん手軽で、書き換えられないなら 6-3 になる。
8-3. 4つの比較
| 入れ方 | リクエスト | 転送量 | 既存要素に当てられるか |
|---|---|---|---|
module/index.js 直読み |
— | — | 動かない |
/+esm |
21 | 237,345 B | ○ |
bundle/budoux-ja.min.js |
1 | 13,579 B | ×(要素で囲む必要あり) |
module/parser.js + モデル |
2 | 11,131 B | ○(テキストのみ) |
| ビルド時に Node で挿入 | 0 | 0 B | ○ |
9. flex の中に span を入れると逆に割れる
9-1. 文節で切ったのに1文字ずつ割れた
ZWSP ではなく <span> で文節を包む実装にした場合(HTMLProcessor の separator を変えるとこうなる)、タップ標的のために display:flex を当てていたリンクで、逆に細かく割れた。
| 入れた span | 自然幅 | 実際の幅 | 描画された行 |
|---|---|---|---|
| 今すぐ | 51.0 px | 46.8 px | 今す / ぐ |
| できる | 51.0 px | 46.8 px | でき / る |
| 応急対処と、 | 102.0 px | 93.7 px | 応急対処 / と、 |
| やってはいけない | 135.5 px | 124.4 px | やってはいけな / い |
| こと | 34.0 px | 31.2 px | こ / と |
5つとも自然幅より縮められ、縮んだ箱の中でそれぞれ改行している。文節で切ったのに、結果は「1文字ずつ割れる」という当初より悪い状態になった。
9-2. 原因はフレックスアイテムの数
CSS Flexible Box Layout Module Level 1 の Flex Items にこう書かれている。
Each in-flow child of a flex container becomes a flex item, and each child text sequence is wrapped in an anonymous block container flex item.
span を入れる前は、見出しのテキスト全体が連続した1つのテキスト列なので、匿名のフレックスアイテム1個にまとめられる。その中では普通のテキストとして流れるから、何の問題も起きない。
span を5個入れた瞬間、フレックスアイテムが5個になる。既定の flex-wrap: nowrap では折り返せないので、5個とも flex-shrink で縮められ、縮んだ幅の中で内部改行が起きる。
9-3. 単体では正しい2つの変更が、組み合わせで壊れた
a { display:flex; min-height:48px; align-items:center } は、WCAG 2.5.8 Target Size を満たすための素直な書き方だ。文節分割も単体では正しい。壊れたのは組み合わせで、どちらのコミットにもバグは無い。
こういう壊れ方は、どちらか一方を入れた時点のテストでは絶対に出ない。新しいDOM操作を足すときに「対象要素の親に flex / grid が当たっていないか」を機械的に確認する以外に、防ぎようがなかった。
9-4. 直し方
高さは padding で作る。
/* × テキストを持つ要素に flex を当てる */
a.card { display: flex; align-items: center; min-height: 48px; }
/* ○ 高さは padding で作る */
a.card { display: block; padding: 14px 0; min-height: 48px; }
display:block に変えて同じ測定をすると、5つとも自然幅のまま2行に正しく流れた。どうしても flex が要る場合は、テキスト全体を1つの span で包んでからその中を分割する(フレックスアイテムを1個に保つ)。
10. コピペで測るスニペット
自分のページで同じことを測るためのスニペット。DevTools のコンソールに貼るだけで動く。
(() => {
// 短い表示テキストだけを対象にする。本文の段落は数えない
const SEL = 'h1,h2,h3,h4,figcaption,th,td,button,dt,summary,a[class*="card"]';
const PUNCT = /[、。,.・:;!?!?)」』】〕〉》]/;
const linesOf = (el) => {
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT), ns = [];
for (let n = w.nextNode(); n; n = w.nextNode()) if (n.textContent.trim()) ns.push(n);
const r = document.createRange(), out = [];
for (const n of ns) {
const t = n.textContent;
for (let i = 0; i < t.length; i++) {
const ch = t[i];
if (ch === '' || ch === '\n') continue;
r.setStart(n, i); r.setEnd(n, i + 1);
const b = r.getBoundingClientRect();
if (!b.width && !b.height) continue;
const L = out.find(l => Math.abs(l.top - b.top) < 4);
if (L) { L.txt += ch; L.right = Math.max(L.right, b.right); }
else out.push({ top: b.top, txt: ch, left: b.left, right: b.right });
}
}
out.sort((a, b) => a.top - b.top);
return out;
};
const bad = [];
for (const el of document.querySelectorAll(SEL)) {
if (!el.offsetParent) continue; // 非表示
const txt = el.textContent.trim();
if (!txt || txt.length > 40) continue; // 長文は対象外
const ls = linesOf(el);
if (ls.length < 2) continue;
for (let i = 0; i < ls.length - 1; i++) {
const prev = ls[i].txt.slice(-1), next = ls[i + 1].txt[0];
const ok = PUNCT.test(prev) ||
/[\s]/.test(prev) ||
/[A-Za-z0-9]/.test(prev) !== /[A-Za-z0-9]/.test(next); // 和欧の境目は可
if (!ok) bad.push({ el, at: prev + '|' + next, lines: ls.map(l => l.txt) });
}
}
console.table(bad.map(b => ({ 折れた位置: b.at, 行: b.lines.join(' / ') })));
console.log('要確認', bad.length, '件 /', window.innerWidth, 'px');
return bad;
})();
10-1. 判定はゆるくしてある
ok の条件は「約物の後」「空白・ZWSP の後」「和文と欧文の境目」の3つだけで、辞書は使っていない。BudouX を読み込まずに済ませるための割り切りで、その分、見逃しではなく誤検出の方向に倒れる(「本当は自然な切れ目なのに数えられる」ケースが出る)。
厳密にやるなら 5-3 の parser.parse() で正解の境界集合を作って突き合わせる。この記事の測定はそちらでやっている。
10-2. 幅を変えて回す
window.innerWidth を出力に混ぜてあるのは、幅を変えると結果が変わるからだ。DevTools のデバイスモードで 375px / 320px / 1280px に切り替えて、それぞれで走らせる。デスクトップだけで測っても意味がない。
10-3. 除外条件を書くときの注意
このスニペットは el.offsetParent が無い要素と40字超の要素を除外している。除外は「見ない」という宣言なので、その除外が隠しうる欠陥を1つ想像して、別の条件で拾えるようにしておく必要がある。
実際にこれで一度やられた。「幅40px未満はスクリーンリーダー用の視覚的非表示だろう」という除外を入れていたら、flexで18px幅まで潰されて1文字ずつ縦に折れていたヘッダーの見出しがその除外に入り、網羅検査を0件で通していた。除外を足すたびに、網羅検査の「0件」の価値は静かに下がる。
11. まとめ
11-1. 測った結果
343px幅・見出し6本・Chrome 152 での文節外改行の数。
| 指定 | 文節外改行 | 追加JS |
|---|---|---|
| 何もしない | 6 / 6 | 0 |
text-wrap:balance |
5 / 6 | 0 |
word-break:auto-phrase(lang なし) |
6 / 6 | 0 |
word-break:auto-phrase(lang="ja") |
1 / 6 | 0 |
BudouX + keep-all(ビルド時に挿入) |
0 / 6 | 0 |
BudouX + keep-all(実行時・パーサのみ) |
0 / 6 | 11.1 KB |
11-2. 持ち帰り
-
text-wrap:balanceは行の長さを均す機能で、日本語の単語境界は守らない。 孤立文字対策としては有効だが、単語が割れる問題の解にはならない。むしろ新しい割れ方を作ることがある -
word-break:auto-phraseはlangが無いと黙ってnormalに落ちる。CSS.supportsはtrueを返し続けるので、機能検出では気づけない -
CSS.supportsが答えているのは「パースできるか」で、「効いているか」ではない。 入力データに依存する機能(auto-phrase、hyphens:auto、font-feature-settings)では、この2つは食い違う -
BudouX は「切ってよい場所」を返すだけの部品なので、ビルド時に済ませられる。 実行時に必要でも
Parserとモデルだけなら 11.1KB(2リクエスト)。/+esmの 237KB は、サーバ用DOM実装まで一緒に落ちてきている - npm の
browserフィールドはバンドラの規約で、ブラウザのモジュール解決には効かない。CDN 直読みでFailed to resolve module specifierが出たらこれを疑う - テキストを持つ要素に
flexを当てない。後から要素を1つ入れただけで、匿名フレックスアイテム1個が複数のアイテムに分かれて組版が変わる - 検査が見ていない欠陥は、存在しないことにされる。孤立文字の検査を通していたから気づかなかった。指摘を受けたら、直す前に「その欠陥を見る検査」を足す
12. 参考
12-1. 一次情報
- UAX #14: Unicode Line Breaking Algorithm
- MDN: text-wrap-style
- MDN: word-break
- MDN: overflow-wrap
- MDN: Range
- caniuse: word-break auto-phrase
- google/budoux(Apache-2.0)
- budoux JavaScript module の README
- CSS Flexible Box Layout Level 1 — Flex Items
- WCAG 2.2 Understanding 3.1.1 Language of Page
- WCAG 2.2 Understanding 2.5.8 Target Size (Minimum)
- 日本語組版処理の要件(W3C)
12-2. 関連
同じ「指定値と表示値が食い違うのに何のエラーも出ない」系の話として、図に15pxで書いた文字が、スマホでは実効7pxで表示されていた話も書いている。
12-3. 次にやること
次にやることを1つだけ選ぶなら、自分のサイトを375px幅で開いて、10章のスニペットをコンソールに貼って試してみてほしい。0件なら何もしなくていいし、件数が出たら <html lang="ja"> が付いているかを確認する。CSS を書き換える判断は、その2つを見てからで間に合う。






