異なるサイズのテキストが並ぶ行は、ベースライン揃えによって各テキストの上端と下端の範囲が組み合わさるため、両方の span の line-height 値を超えることがあります。このガイドでは、その余分な高さを調べ、Mac のデスクトップブラウザーで2つのラベルが並ぶ行を調整する方法を説明します。

最も高い span で行の高さが決まらないのはなぜですか?

大きな Label A の隣に小さな Label B がある、抽象的なページヘッダーを考えてみましょう。両方の span に line-height: 40px を指定しても、それらを含む行の高さは40 CSSピクセルを超えることがあります。これは、インラインレイアウトで、行を囲む領域を計算する前にボックスをどのように揃えるかによるものです。

通常のインラインテキストでは、ブラウザーは各ボックスが高さに与える分を計算し、ボックスを垂直方向に揃えてから、それらの範囲全体を囲みます。ベースライン揃えの場合、最も高い上端と最も低い下端が別々の span に属することがあります。そのため、結果が個々の寄与分のどちらをも上回ることがあります。また、親要素も最小サイズの見えないボックスを加えます。これは従来 ストラット と呼ばれています。W3C: 行の高さの計算

ベースラインはテキストを揃える基準です。行の高さへの寄与分の垂直方向の中点ではありません。サイズの異なるテキストに同じ行の高さを指定しても、ベースラインの上下に両方のテキストが等しい距離で収まるわけではありません。初期の vertical-align 値は baseline です。MDN: vertical-align

次の3つの寸法は区別してください。フォントのアセントとディセントのメトリクス、個々の文字の可視範囲、そして行レイアウトに加わるスペースです。span の色付き背景や選択範囲の矩形を、行全体を囲むボックスの直接的な測定値とみなしてはいけません。CSS では、インライン要素の描画範囲と行の高さの計算を区別しています。W3C: リーディングとハーフリーディング

40px の寄与分が2つあるのに、行の高さが48.4pxになるのはなぜですか?

明示的な行の高さを持つ通常のベースライン揃えテキストでは、次の計算が役立ちます。行の高さからフォントのアセントとディセントの合計を引き、残ったスペースを上下に均等に配分します。この余分なスペースをリーディングと呼び、その半分をハーフリーディングと呼びます。CSS Inline Layout Level 3 の作業草案では、この計算を説明し、負のリーディングも認めています。草案であることは、そこに含まれる新しいプロパティすべてがブラウザーでサポートされていることを示すものではありません。W3C: レイアウト境界の計算

次の値は説明用であり、Arial や Mac のいずれかのフォントを測定した値ではありません。ほかのインラインコンテンツや垂直方向のオフセットがなく、親のストラットの寄与分が小さい span と一致すると仮定します。

寄与要素 フォントのアセント フォントのディセント 行の高さ ハーフリーディング ベースラインより上 ベースラインより下
大きい span 32px 8px 40px 0px 32px 8px
小さい span 9.6px 2.4px 40px 14px 23.6px 16.4px
親のストラット 9.6px 2.4px 40px 14px 23.6px 16.4px

このベースライン揃えの例では、それぞれの側で最大の範囲を選びます。max(32, 23.6, 23.6) + max(8, 16.4, 16.4) = 48.4px。各寄与分の合計は40pxですが、上端は大きい span が決め、下端は小さい span と親が決めます。この計算例は、ブラウザーでの測定値を予測したものではなく、文書に記載されたレイアウト計算に基づいています。W3C: インラインレイアウトモデル

診断の際に問うべきことは、「各端をどの要素が決めているか?」です。小さい span の行の高さを下げても、その下端への寄与分と同じ寄与分が親のストラットから生じていれば、影響はありません。

Mac で再現して調べるにはどうすればよいですか?

この手順では Mac の Chrome を使います。新しいタブを開き、about:blank に移動してから、Command + Option + J を押して Console を開きます。Chrome ではこのショートカットで JavaScript を実行できます。Chrome DevTools: 概要

次のプログラム全体を空のタブで実行してください。同じラベルを使った3つのサンプル行を作成し、測定値を出力します。また、シャドウルートを使ってデモのスタイルをひとまとめにします。パッケージのインストールも外部ライブラリも必要ありません。もう一度実行すると、前回のデモが置き換わります。

(async () => {
  document.getElementById('line-box-demo')?.remove();
  const host = document.createElement('div');
  host.id = 'line-box-demo';
  document.body.append(host);
  const root = host.attachShadow({ mode: 'open' });
  const style = document.createElement('style');
  style.textContent = `
    :host { display: block; padding: 24px; color: #222; background: #fff; }
    .label { margin: 20px 0 6px; font: 14px Arial, sans-serif; }
    .row { font: 12px/40px Arial, sans-serif; white-space: nowrap;
      margin: 0; padding: 0; border: 0; background: #e8eef4; }
    .large { font-size: 40px; line-height: 40px; }
    .small { font-size: 12px; line-height: 40px; }
    .trim-span .small, .trim-both .small { line-height: 12px; }
    .trim-both { font-size: 40px; }
  `;
  root.append(style);
  const cases = [
    ['', 'Shared 40px line-height'],
    ['trim-span', 'Smaller span only'],
    ['trim-both', 'Parent and smaller span']
  ];
  const rows = cases.map(([className, label]) => {
    const caption = document.createElement('p');
    caption.className = 'label';
    caption.textContent = label;
    const row = document.createElement('div');
    row.className = `row ${className}`;
    for (const [size, text] of [['large', 'Label A'], ['small', ' Label B']]) {
      const span = document.createElement('span');
      span.className = size;
      span.textContent = text;
      row.append(span);
    }
    root.append(caption, row);
    return { label, row };
  });
  await document.fonts.ready;
  console.table(rows.map(({ label, row }) => ({
    case: label,
    parentLineHeight: getComputedStyle(row).lineHeight,
    largeLineHeight: getComputedStyle(row.children[0]).lineHeight,
    smallLineHeight: getComputedStyle(row.children[1]).lineHeight,
    rowHeight: row.getBoundingClientRect().height
  })));
})();

このプログラムは document.fonts.ready を待ちます。これは、ドキュメントのフォント読み込みと関連するレイアウト処理が完了した後に解決されます。後から別のフォントを使ったり、コンテンツを変更したりした場合は、その変更が反映されてから再度測定してください。MDN: FontFaceSet.ready

3つの行を、条件を制御した比較として読み取ります。1行目では、両方の span と親に40pxの行の高さを指定します。2行目では、小さい span だけの行の高さを12pxに下げます。3行目ではさらに、親のフォントサイズを40pxに変更し、ストラットのフォントサイズと行の高さを大きいテキストに合わせます。2行目は高いままで3行目が縮むなら、この比較によって親の寄与が原因だと特定できます。

説明用の48.4pxという値を期待せず、ブラウザーで得られた結果を使ってください。このプログラムは getBoundingClientRect() で各行を測定します。このメソッドは、パディングと境界線を含む要素の境界矩形を返します。ここでは各行にパディングも境界線もなく、折り返しのない1行のテキストが入り、高さは自動です。そのため、行の高さを推定するうえで有用な値になります。同じAPIで span を測定すると、span の矩形が測定されるだけで、行全体を囲むボックスが直接わかるわけではありません。MDN: getBoundingClientRect()

既存のレイアウトでは何を確認すべきですか?

対象のテキストを Control キーを押しながらクリックし、Inspect を選びます。Elements で両方の span と、それらを含むブロックを調べます。Styles で宣言をたどり、Computed で適用された値を確認します。継承されたプロパティを調べる必要がある場合は Show All を有効にしてください。Chrome ではこれらのビューとプロパティフィルターが提供されています。Chrome DevTools: CSS 機能リファレンス

各要素について font-family、font-size、line-height、display、vertical-align を記録します。2つの span だけを見るのではなく、まず親から確認してください。その後、次の手順で原因を切り分けます。

  1. レイアウトモデルを確認する。 子要素が通常のインラインフローに参加しているのか、flex または grid のアイテムなのかを確認します。
  2. 1行であることを確認する。 折り返しによって測定が紛らわしくならないよう、幅の広いテスト領域と短いサンプルラベルを使います。
  3. 揃え方を確認する。 上記の簡単な計算を使う前に、ベースラインのずれや標準以外の垂直揃えを一時的に取り除きます。
  4. 寄与要素を1つずつ変更する。 小さい span の行の高さを下げて測定し、次に親のフォントと行の高さの寄与を個別に調べます。
  5. 実際の条件に戻す。 原因を特定したら、意図したフォント、利用可能な幅、テキストで再確認します。

継承も確認してください。単位のない line-height: 1.2 は倍率として子孫に継承され、各子孫は自身のフォントサイズにその倍率を適用します。40px のような長さは、上書きされない限り固定の長さとして継承されます。パーセント値は、それを宣言した要素のフォントサイズを基準に計算されます。値の選び方によっては、小さいテキストに予想以上に大きなリーディングが付くことがあります。normal はフォントとブラウザーによって変わるため、固定の数値を前提にするのには適しません。MDN: line-height の値と継承

意図した結果にはどの調整が合いますか?

2つのラベルが何を表すかに応じて、調整方法を選びます。ラベルが別々の要素として並ぶコンパクトなヘッダーと、折り返しのある段落に埋め込まれた単語とでは、要件が異なります。

意図する結果 試す調整 確認すること
テキストのベースラインを揃える 小さい span に適切な行の高さを指定し、親のフォントメトリクスを主要なテキストに合わせる 親のストラットが不要な下端を作らなくなっていること
別々のラベルの上端を揃える 両方のインライン span に vertical-align: top を指定して試す 変更後のベースラインの関係がデザインに合うこと
別々のラベルを行内で中央揃えにする align-items: center を指定した flex コンテナーを使う 狭い幅での表示や項目の折り返しがレイアウトに合うこと
連続した段落の流れを維持する インラインテキストをそのまま使い、意図した継承の行の高さを指定する 生成されるすべての行が異なるサイズのテキストを収められること

vertical-align: top は、要素とその子孫を行の上端に揃えます。middle は別の定義で、要素の中点を親のベースラインと親の x-height の半分を足した位置に揃えます。そのため、行全体の幾何学的な中央に揃えるという意味ではありません。また、揃え方を変えても、最終的な高さが特定の値になるとは限りません。MDN: vertical-align の値

独立した2つのラベルには、flexbox の align-items を使うと、交差軸方向の揃え方を明示できます。中央揃えとベースライン揃えは、異なるデザイン上の選択です。flexbox に切り替えるとレイアウトのコンテキストが変わるため、ラベルが独立した項目である場合に使い、狭い幅での表示も確認してください。MDN: align-items

行の高さを小さくすると、テキストがレイアウト上の寄与範囲を越えて描画されることがあります。間隔を詰めた後は、隣接する行も確認してください。測定上のボックスがコンパクトになっただけでは、重なりなくテキストが収まるとは限りません。W3C: リーディングとハーフリーディング

Canvas の測定でブラウザーの行レイアウトを置き換えられますか?

Canvas の測定でわかるのは、関連する別の内容です。WHATWG は measureText() を定義し、フォントのアセントとディセントのメトリクスと、個々のテキストの実際の境界範囲を区別しています。Canvas の描画には有用な測定値ですが、字形の境界測定には、周囲にある DOM の親のストラット、インライン揃え、行の高さへの寄与は含まれません。この行の高さを予測する際に、これだけを使わないでください。WHATWG: TextMetrics

実験する場合も、テキストのシェーピング境界を保ってください。有効な書式変更がなければ、CSS ではインライン境界をまたいでシェーピングを継続する必要があります。一方、非初期の垂直揃えなど、一部の境界ではシェーピングを分断する必要があります。文字ごとに分割して測定するのは避けてください。書記素クラスタや合字を分割すると、一貫しない結果になることがあります。ラベル全体または意味のあるテキスト範囲をそのまま保ってください。W3C: 要素境界をまたぐテキスト処理とシェーピング

余分な高さを解消するには、親も含め、上端と下端をそれぞれどの要素が決めているかを特定します。行の高さへの寄与を調整するか、ラベルの用途に合う揃え方を選び、意図したテキストとフォントを使って行全体をもう一度測定してください。

出典