ブラウザのテキストプレビューのサイズを決めるには、目的のビューポート幅でレイアウトされた DOM 要素を測定します。1 行のテキストのメトリクスには Canvas を使います。このガイドでは、Mac で両方を比較し、幅と高さが異なる理由を説明します。

実際の作業では、ブラウザのビューポートが変化するとき、合成カードのラベルに必要なスペースを調べます。役立つ結果を得るには、ビューポート、利用可能なテキスト幅、フォント設定、測定したボックスをまとめて記録します。こうした条件を伴わない幅は、再現が困難です。

どのテキストの幅と高さを測定すべきか?

ブラウザ API は、サイズに関するさまざまな問いに答えます。計算する前に、必要な判断に合った測定方法を選びましょう。

知りたいこと 測定方法 表すもの
折り返されない 1 つの文字列が水平方向にどれだけ進むか? Canvas measureText(text).width テキストの送り幅
Canvas のグリフ形状の境界はどこか? Canvas の actualBoundingBox… プロパティ 配置点とベースラインを基準にしたグリフの境界
カードが占めるレイアウト領域はどれくらいか? 要素の getBoundingClientRect() パディングと境界線を含む、その要素の境界ボックス
折り返し後のテキストフラグメントはどこにあるか? Range の getClientRects() 選択したテキストに対応する矩形
そのテキストフラグメントを囲む矩形はどれか? Range の getBoundingClientRect() それらをまとめた境界矩形

Canvas の送り幅とグリフ幅は異なる場合があります。イタリック体の形状は送り幅を超えて伸びることがあります。グリフの境界を求めるには、幅を actualBoundingBoxLeft + actualBoundingBoxRight、高さを actualBoundingBoxAscent + actualBoundingBoxDescent として計算します。これらの測定値の単位は CSS ピクセルです。TextMetrics — MDN

要素の測定値にはパディングと境界線が含まれますが、テキストだけを対象にする Range は、フォントメトリクスを使って選択テキストを測定します。どちらも、実際に描画されたピクセルの正確な矩形と表現すべきではありません。W3C の仕様では、水平方向の Range テキスト矩形はアセント、ディセント、テキストの送り幅を使って定義されています。Element: getBoundingClientRect() method — MDN、CSSOM View Module — W3C

ブラウザのサイズプレビューでは、領域の割り当てに要素のボックスを使い、テキストの流れの確認に Range の矩形を使います。Canvas の測定値は、折り返されない文字列の参考値として別に扱います。

まずビューポートとテキストの条件を設定する

Mac の Chrome で DevTools を開き、Toggle device toolbar を有効にして、Dimensions から Responsive を選択します。ビューポート欄に目的の幅と高さを入力します。Chrome では数値の入力と、ドラッグによるビューポートのサイズ変更の両方ができます。デスクトッププレビューでは、その設定が使える場合はデスクトップのデバイスタイプを選択します。Simulate mobile devices with device mode — Chrome for Developers

ビューポート幅とテキスト幅を区別してください。カードには最大幅、外側の余白、パディング、境界線がある場合があります。ブラウザの幅を広げても、カードが最大幅に達するとテキスト領域はそれ以上広がりません。

たとえば、カードの境界ボックス幅が例として 360px、左右のパディングがそれぞれ 16px、境界線がそれぞれ 1px だとします。利用可能なコンテンツ幅は 360 − 32 − 2 = 326px です。これは折り返しに使える幅です。要素の境界矩形は引き続き 360px を返します。これは、文書化されている境界ボックスの測定動作によるものです。Element: getBoundingClientRect() method — MDN

目的に合ったフォントファミリー、ウェイト、サイズ、スタイル、行の高さ、文字間隔、折り返し規則を設定します。特に、white-space: normal は空白をまとめて折り返しを許可します。一方、pre-wrap は空白とソース内の改行を保持しながら折り返しを許可します。一方の設定を使ったプレビューでは、もう一方を正確に再現できません。white-space — MDN

また、フォントサイズと行の高さも区別してください。横書きでは、line-height は行ボックスの高さの決定に関わりますが、文字の見た目の高さを示すものではありません。そのため、フォントが 16px で行の高さが 24px の場合、Canvas のグリフ境界とは異なる方法で高さを計算する必要があります。line-height — MDN

合成テキストのプレビューを実行する

空のタブを開いて DevTools を開き、Console を選択して、以下の完全なブラウザスクリプトを 1 回実行します。中立的なカードと測定レポートが作成されます。ライブラリ、外部フォント、既存のセレクター、HTML ファイルは必要ありません。別のコピーを実行する前に、空のタブを再読み込みしてください。

カードは、境界ボックス幅が 480px に達するまでビューポートに合わせて広がります。ウィンドウまたはエミュレートされたビューポートのサイズを変更すると、レポートが更新されます。この例は、均一なスタイルを使った単純な横書きテキストブロックに限定しています。ブラウザに依存するコードは確認済みですが、準備中にブラウザで実行したものではありません。

(async () => {
  const text =
    "Sample collection: round forms, soft gradients, and simple patterns.";

  document.body.style.margin = "0";

  const card = document.createElement("div");
  card.textContent = text;
  card.lang = "en";
  card.dir = "ltr";
  card.style.cssText = [
    "box-sizing: border-box",
    "width: min(480px, calc(100% - 48px))",
    "margin: 24px",
    "padding: 16px",
    "border: 1px solid #777",
    "font: normal 400 16px/24px Arial, sans-serif",
    "font-kerning: normal",
    "letter-spacing: 0px",
    "word-spacing: 0px",
    "white-space: normal",
    "overflow-wrap: normal",
    "word-break: normal",
    "hyphens: none",
    "color: #222",
    "background: #f4f4f4"
  ].join(";");

  const report = document.createElement("pre");
  report.style.cssText =
    "margin: 24px; white-space: pre-wrap; overflow-wrap: anywhere;";
  document.body.append(card, report);

  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    report.textContent = "A Canvas 2D context is unavailable.";
    return;
  }

  // Establish layout before waiting for the fonts used by that layout.
  card.getBoundingClientRect();
  await document.fonts.ready;

  const format = value =>
    Number.isFinite(value) ? value.toFixed(2) : "unavailable";

  function measure() {
    const style = getComputedStyle(card);
    const box = card.getBoundingClientRect();
    const horizontalExtras =
      parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) +
      parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth);

    const range = document.createRange();
    range.selectNodeContents(card);
    const textBox = range.getBoundingClientRect();
    const fragments = Array.from(range.getClientRects());

    ctx.font = "normal 400 16px Arial, sans-serif";
    ctx.fontKerning = "normal";
    ctx.direction = "ltr";
    ctx.textAlign = "left";
    ctx.textBaseline = "alphabetic";
    const metrics = ctx.measureText(text);
    const inkWidth =
      metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight;
    const inkHeight =
      metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;

    report.textContent = [
      "All dimensions below are CSS pixels.",
      `Layout viewport: ${document.documentElement.clientWidth} x ${document.documentElement.clientHeight}`,
      `Card border box: ${format(box.width)} x ${format(box.height)}`,
      `Available text width: ${format(box.width - horizontalExtras)}`,
      `Range bounds: ${format(textBox.width)} x ${format(textBox.height)}`,
      `Range fragment count: ${fragments.length}`,
      `Canvas unwrapped advance: ${format(metrics.width)}`,
      `Canvas glyph bounds: ${format(inkWidth)} x ${format(inkHeight)}`,
      `Computed line height: ${style.lineHeight}`
    ].join("\n");
  }

  measure();
  window.addEventListener("resize", measure);
})().catch(error => console.error("Preview failed:", error));

要素と Range の呼び出しは、ブラウザの実際のレイアウトを使います。Canvas は同じ合成文字列を、折り返しのない 1 つの文字列として測定します。HTML Standard では、Canvas のテキスト処理時に ASCII の空白文字をスペースに置き換えます。measureText() に改行を渡しても、複数行からなる CSS の段落にはなりません。HTML Standard: The canvas element — WHATWG

スクリプトはレイアウトを確定してから document.fonts.ready を待ちます。MDN によると、この Promise は、使用されているフォントに関する読み込みとレイアウトの処理が完了すると fulfilled になります。これは、宣言されたすべてのフォントが読み込まれたことや、最初に優先された書体が実際に表示されることを意味しません。Document: fonts property — MDN

結果を読み取り、テキストが収まるか判断する

まず Available text width を確認し、次に 2 種類のビューポートサイズでカードの高さを比較します。折り返しによって行が増えると、カードの高さは変わりますが、Canvas の折り返しなしの送り幅は変わらないことがあります。この組み合わせは、折り返しの変化とフォントの変化を見分けるのに役立ちます。

この例の以下の幅は、測定したフォントの結果ではなく、説明用の計算値です。レイアウトビューポートの幅が記載の値で、スクリプトのスタイルが変わらないことを前提としています。

レイアウトビューポート幅 カードの境界ボックス幅 利用可能なテキスト幅
360px 312px 278px
800px 480px 446px
1200px 480px 446px

最後の 2 行は、ブラウザを広げてもテキストの折り返しが変わらない理由を示しています。カードはすでに最大幅に達しています。ビューポートだけを変えても、カードのコンテンツ領域は広がりません。

均一なテキストだけを含む単純なブロックでは、行の高さが 24px の場合、例として 3 行のレイアウトは 72px の行ボックス領域を占めます。垂直方向のパディング 32px と境界線 2px を加えると、カードの高さは例として 106px になります。この計算は結果の理解に役立ちますが、コンテンツやスタイルが複雑になった場合は、測定した DOM の高さを判断の基準にしてください。line-height — MDN

Range fragment count を普遍的な行数として自動的に扱わないでください。Range API は、選択規則に従ってテキストと要素の矩形を返します。リッチテキスト、入れ子になった要素、分割されたレイアウトでは、矩形の組み合わせがさらに複雑になることがあります。このサンプルでは、診断内容を理解しやすくするため、単一のプレーンテキストノードだけを選択します。CSSOM View Module — W3C

同様に、ceil(unwrappedWidth / availableWidth) を使って折り返した高さを推定するのも避けてください。この式は、テキストを水平方向の任意の位置で改行できることを前提としています。ブラウザの折り返しは、設定された空白と改行の動作に従うため、折り返し後のブロックを測定してください。white-space — MDN

測定値が一致しないときに確認すること

まず測定範囲を確認してください。幅いっぱいのカードの矩形と、それより狭い Range の矩形は、どちらも正しい場合があります。カードはレイアウト領域を確保し、Range は選択したテキストを表します。Canvas のグリフ高さも、目的の異なる 3 つ目の値です。

次にフォントの読み込み状態を確認します。宣言した Web フォントを使う場合は、document.fonts.load(fontSpecification, sampleText) で測定前に一致するフォントフェイスを明示的に要求できます。成功した結果に黙って置き換えるのではなく、拒否を処理してください。省略可能なテキスト引数は Unicode の範囲で絞り込みますが、個々のグリフが含まれるかは確認しません。FontFaceSet: load() method — MDN

ラベルの幅を計算するときは、シェーピングの境界を保ってください。個別に測定した文字幅を合計せず、文字列全体を測定します。Canvas 仕様は、現在のフォントとテキスト設定を使って、配置済みのグリフ列を準備します。その文字列の幅には、この結果を使います。このサンプルの Canvas 比較は記載されたフォント設定に限定し、あらゆる CSS タイポグラフィ機能を再現すると想定しないでください。HTML Standard: The canvas element — WHATWG

既存のページでは、実際のコンポーネントをその場で測定してください。テキストを汎用の非表示ボックスに移すと、関連するスタイルと包含コンテキストも再現しない限り、レイアウト条件が変わります。レイアウトボックスに関連付けられていない要素からは、通常のボックス測定に役立つ値が得られません。また、CSS の変換はこれらの API が返す矩形に影響します。この合成例では、どちらの問題も避けています。CSSOM View Module — W3C

複数のブラウザサイズでプレビューを繰り返すには

たまに確認するだけなら、ビューポートサイズを手動で入力し、レポートを記録します。非常に狭いレイアウトと非常に広いレイアウトだけを確認するのではなく、折り返しが変わる境界付近の幅を比較してください。

繰り返し確認する場合、Playwright は明示的なビューポート寸法と、独立した deviceScaleFactor 設定に対応しています。また、デバイスプリセットのビューポートを上書きできます。これは、サポートされている自動化手段です。ビューポートを設定してページを読み込み、フォントと関連コンテンツを待ってから、ブラウザで DOM の測定値を取得します。Emulation — Playwright

CSS の寸法とディスプレイのピクセル密度は別に扱ってください。Playwright ではビューポートとスケール係数が別の設定として公開され、Canvas のテキストメトリクスは CSS ピクセルで表されます。ディスプレイの密度が高くても、それだけでラベルに割り当てる CSS 幅を 2 倍にする必要はありません。Emulation — Playwright、TextMetrics — MDN

そのため、役立つプレビュー結果は、ブラウザ、ビューポート、テキスト、タイポグラフィ、コンテンツ幅、測定したカードの高さをまとめた簡潔な記録です。折り返しのあるコンポーネントのサイズ決定には DOM の結果を使い、1 行の文字列に関する問いには Canvas のメトリクスを使い、表示を再現する必要のある各ブラウザで測定を繰り返してください。

参考資料