こなさんみんばんわ。
はじめて生成 AI (Grok) に OGP 画像を生成させてみたら、ものの数分で思い描いていたような画像を生成してくれて草。

そんな話はさておき、先日リリースされた Google Chrome バージョン 140 の新機能のひとつに CSS typed arithmetic(CSS 型付き算術1)というのがあります。簡単にいうと、今までの CSS の calc() 関数ではできなかった単位の付いた値同士の割り算ができるようになったというもので、これにより calc() 関数を使って単位のない数値を算出することが可能になりました。

Safari では v18.2 ですでに実装済み、Edge でも同じく v140 でこの機能がサポートされております2が、本日はその実践的なユースケースとして「単位のない line-height 値を要素のフォントサイズに応じて動的に算出する」というのを Pure CSS でシンプルに書いてみたいと思います。

…と、これだけの文章ではどういうことかよく分からないかもしれませんが、実例を見ていただければどれだけ便利で、かつ直感的に短く書けるかがお分かりいただける(のではないか😅)と思いますので、よろしければご一読くださいませ。

前提: この記事に登場するキーワード

まぁこの記事タイトルに惹かれて読みに来るような方が calc()line-height のことを知らないとは到底思えない訳ですが、いちおう記事を読む上で出てくるキーワードについて、最低限これくらいは分かってないとチンプンカンプンかも…と思うものをリストアップして、簡単にその解説や、理解するのに役立つ 参考 URL を加えておきたいと思います。知ってる方は読み飛ばして本題へどうぞ

calc() 関数

calc() は CSS 上で四則演算ができる関数です。単位が違っても足したり引いたりできて3便利なのはもちろん、最近は式の中で CSS 変数(カスタム・プロパティ)や三角関数なども使えるようになってより便利になりました。よりくわしい解説やその構文サンプル、注意点などについては MDN の記事calc() - CSS: カスケーディングスタイルシート | MDNをご覧ください。

font-size プロパティ

さすがに説明するまでもないかもしれませんが…その名のとおり、Web ページの要素が使用する文字(フォント)の大きさを指定するための CSS プロパティが font-size です。解説やその構文サンプル、注意点などについては同じく MDN の記事font-size - CSS: カスケーディングスタイルシート | MDNをご覧ください。

line-height プロパティ

文字の大きさ = 行の高さでは間が詰まって文章が読みにくいことこの上ないので、普通は文字の大きさに対して 1.5 〜 2 倍程度のサイズにされるものですが、CSS でこの行ボックスの高さを指定するのが line-height プロパティです。解説や(以下略)は MDN の記事line-height - CSS: カスケーディングスタイルシート | MDNへ。

CSS の定義におけるレディング、ハーフ・レディング

で、その行ボックスの高さと文字サイズの差はスペースとなってテキストの上下に現れる訳ですが、そのスペースの合計が CSS においてレディング (leading) と呼ばれるもので、さらにそれを半分にしたものがハーフ・レディング (half-leading) となります。このあたりはウェブデザインにおけるline-heightについて | Rriverという記事で図解入りで分かりやすく解説されておりますので、僕の文章だけでは分からなかったという方はご参考になさってください。涙

emlh そして remrlh

これらはいずれもフォントを基準にした長さの単位(<length> 型)です。emlh はそれぞれその要素自身の font-size および line-height の計算値、また remrlh はそれぞれルート要素(html)における 1em および 1lh に相当します。em, rem にはそれ自身の font-size に使用される場合の例外があります。詳しくは<length> - CSS: カスケーディングスタイルシート | MDNにて。

単位なしの line-height 値を calc() 関数で算出したい

さて、ここからが本題。ちょうどこのサイトで以前から実際に採用しているタイポグラフィの調整方法がありまして、今回はそれを題材にしてみようと思います。

ただ文字サイズ大きくしただけだと行間めっちゃ広くなる問題

このサイトはルート要素の文字サイズを 100%(ユーザーの環境にも依存するけどおおむね 16px)、line-height2 にしていますが、各ページの冒頭にある記事タイトルの文字サイズは、スマホなどの狭い画面では本文の 2 倍、タブレットやパソコンなどの広い画面では 2.5 倍になるように設定しています。

/* このサイトの CSS そのままではないけど、やってることは一緒 */
html {
  font-size: 100%;
  line-height: 2;
}
/* 記事タイトルの class */
.page_header-title {
  font-size: 200%;

  @media (width >= 48em) {
    font-size: 250%;
  }
}

ですが、この場合ただ単に上のコード例のように font-size: 250% としただけだと、見出しが長くて複数行に渡るような場合に、その行間がものすごく空いてしまいます。本文の文字サイズが 16px だとしてその 2.5 倍、さらに line-height が 2 倍ですから、単純計算で 40px のレディングです。そりゃ広いわってなります。

See the Pen フォントサイズだけ拡大した h1 要素のサンプル by Jeffrey Francesco (@jforg) on CodePen.

これだけ広いとやはり本文の行間との差も相まってとても違和感を感じますし、単純に読みづらいというのもあります。なので、ここは line-height の方も調整して見出しの行間を整えたいところです。

文字サイズが変わっても行間は本文と変わらないようにしてみる

その調整方法のひとつとして現在このサイトで採用しているのが、見出しの line-height を常にその文字サイズ + ベースとなる本文のレディング分にするというものです。ウチを例にして具体的に説明すると、

  1. 本文の font-size16px, line-height はその 2 倍で 32px
  2. ベースとなる本文のレディング長は 32px - 16px16px
  3. 見出しの font-size250% なので 16px * 2.5 = 40px
  4. 見出しの line-height は 3. の 40px に 2. の 16px を加えた 56px となる

という流れですね4。そして、できればこれを他の要素に関しても自動で算出できるようにしたいところです。昔はそのようなことは CSS プリプロセッサー(Sass など)の関数を使わないとできませんでしたが、今は Pure CSS でたった一行で実現できます:

h1 {
  line-height: calc(1em + (1rlh - 1rem));
}

いちおう説明しておきますと、rlh が本文の line-height 計算値、1rem が本文の font-size 計算値ですから、その差 1rlh - 1rem は本文のレディング部分に相当する長さとなります。その計算結果を要素自身の文字サイズである 1em に足せば「要素の文字サイズ + 本文のレディング」が計算されます。あとはそれを line-height にすればいいだけです。

👇実際に適用してみたデモです:

See the Pen フォントサイズと一緒に line-height を調整した h1 要素のサンプル by Jeffrey Francesco (@jforg) on CodePen.

このコードの良いところは、もちろん Pure CSS なので都度コンパイルなどをしなくていいというのもありますが、何よりメディアクエリーなどを使ってレスポンシブに文字サイズを変更する場合でも line-height は設定し直さなくていいところですね。1em は常にその要素自身の文字サイズを表しますから、文字サイズが変わればブラウザが再計算してくれるはずなので。

最初に挙げたこのサイトのコード例でいえば、line-height が必要なのは .page_title-header 直下のみで、@media ルール内には必要ありません。書かなくてもちゃんとその条件下での計算値である 56px が適用されます。

.page_header-title {
  font-size: 200%;
  line-height: calc(1em + (1rlh - 1rem));
  /* ↑これだけ書けば */
  /* @media の外側では 32px + 16px = 48px */

  @media (width >= 48em) {
    font-size: 250%;
    /* 内側では 40px + 16x = 56px になる */
  }
}

この CSS コード唯一の問題点は「ピクセル値であること」

ですがこのコード、おおむね期待どおりの機能はするんですがひとつだけ問題点がありまして、それは calc() 関数の結果としてピクセル値が算出されるというところ。特に今回のように line-height に対してピクセル値(に限らず単位のある <length-percentage> 値)が使用されると、場合によっては予期せぬ結果を引き起こすことがある…というのはよく知られているところです。

See the Pen `calc(1em + (1rlh - 1rem))`の結果はピクセル値になる by Jeffrey Francesco (@jforg) on CodePen.

このデモは分かりやすいようにあえて極端になるような設定にしていますが😅 要はこのように親要素の line-height 値(ピクセル)が子の <strong> 要素にそのまま継承されるため、その文字サイズによっては文字が重なって読めなくなるということが起こる訳ですね。前提のセクションで挙げた MDN の記事でもline-height の値は単位なしの数値が好ましいとされており、これは CSS のベスト・プラクティスのひとつといってもいい内容です。

でも今までの calc() 関数ではこれが限界でした…

「それやったらお前、ピクセルの絶対値から比率を示す単位なしの数値に変換したったらええんちゃうんけ!」ということで、先ほどのコードを修正してこういう計算をしたくなりませんか? 実は僕これ最初やってしまってました😅

h1 {
  /* 行ボックスの高さを文字サイズで割ったら
     その比率が数値として出るはずや! */
  line-height: calc((1em + (1rlh - 1rem)) / 1em);
}

ですが、残念ながらこれは動作しません(でした)。なぜなら今までの calc() 関数では掛け算と割り算の場合、片方の値は単位なしじゃないとダメだったからです。これは CSS Values and Units Module Level 3 の §8.1.2その日本語訳)にて規定されており、MDN にも次のような記載があります。

現在の実装では、* および / 演算子では、オペランドの 1 つが単位なしである必要があります。/ の場合、右のオペランドが単位なしでなければなりません。例えば、font-size: calc(1.25rem / 1.25) は有効ですが、font-size: calc(1.25rem / 125%) は不正なコードです。

なので、今までは単位なしの line-height 値を自動で算出するためには、これまで同様に Sass などの関数を使うか、もしくは単位なしの値が入ったカスタム・プロパティをいくつか定義しておいて、それを使って計算するくらいしか方法はなかった訳です。

:root {
  /* 基準値 */
  --base-line-height: 2;
  /* `calc(1rlh - 1rem)` に相当する部分
     ルートの文字サイズ倍率は常に基準 = 1 となるのでこれで OK */
  --base-leading-size: calc(var(--base-line-height) - 1);

  /* 要素の font-size をベースからどれだけ拡大するかを入れておく */
  --font-scale-h1-wide: 2.5;
  --font-scale-h1-narrow: 2;
}
html {
  font-size: 100%;
  line-height: var(--base-line-height);
}
.page_header-title {
  /* メディアクエリ対応ができるように独立した変数を用意 */
  --font-scale: var(--font-scale-h1-narrow);

  font-size: calc(var(--font-scale) * 100%);
  /* `calc((1em + (1rlh - 1rem)) / 1em)` に相当する部分 */
  line-height: calc((var(--font-scale) + var(--base-leading-size)) / var(--font-scale));

  @media (width >= 48em) {
    /* これだけ変更すれば `line-height` は自動で追従 */
    --font-scale: var(--font-scale-h1-wide);
  }
}

こうやってがんばれば実現できないこともないですが、これにさらに他の見出しレベルだとか、見出し以外の文字サイズが変わる要素が加わることを考えると、コード長くて見通しも悪くなりそう…というのは容易に想像つきますね。あと単純に面倒くさい(´・_・`)

CSS 型付き算術 = 次期仕様に従ったアップグレード版 calc()

そんな calc() 関数における掛け算・割り算でのオペランドに対する制限が、次の仕様である CSS Values and Units Module Level 4 では緩められることになりました。ワーパチパチ
これは Level 3 と同じく “Type Checking” のセクション(セクション番号は §10.9 に変わりますが)に Note として記載されています。以下は日本語訳からの抜粋:

注記: この仕様の以前までのバージョンでは、[ 乗算/除算 ]がとれる引数は制限されていた — 複階的な中間結果( 1px * 1em の次元は <length> の 2 乗になるなど)が生産されるのを避けるため / 0 による除算を構文解析時に検出可能にするため。 このバージョンでは、 その制約は緩められる。

今回 Chrome v140 に入った CSS 型付き算術 (CSS typed arithmetic) というのはつまり、この新しい仕様に従って実装されたアップグレード版の calc() 関数ということですね。冒頭にも書いたとおり Safari や Edge でも導入がされておりますので、他の Chromium ベースのブラウザも含めればかなりの環境で、次の CSS コードが動作するということになります。

/* 前セクションに例示したコードはシンプルにこうできる */
html {
  font-size: 100%;
  line-height: 2;
}
/* 記事タイトルの class */
.page_header-title {
  font-size: 200%;
  line-height: calc((1em + (1rlh - 1rem)) / 1em);

  @media (width >= 48em) {
    font-size: 250%;
  }
}

超すっきり😀

また、実際に対応ブラウザで次のデモを見ると、前のデモとは違って <strong> 要素内のテキストが重ならずに表示されるかと思います。これで line-height を計算している calc() 関数がちゃんと単位なしの値を算出していることも、お分かりいただけるのではないでしょうか。

See the Pen `calc((1em + (1rlh - 1rem)) / 1em)`の結果は単位のない数値になる by Jeffrey Francesco (@jforg) on CodePen.

でも実際に問題なく使えるのはまだ先のことになりそう

以上、ここまで CSS 型付き算術のお話をしてきました。個人的にはそれこそ今すぐにでも使えるとありがたい機能なんですが、残念なことに まだ Firefox にはこの機能が入ってないですし、近々入りそうな風でもないのですよね…(´・_・`)

これから実装されたとしても今年中に間に合うかといったところでしょうから、早くてもおそらく Baseline 2026 とか 2027 くらいになりそうですし、そうなると実際に問題なく使えるようになるのは 2, 3 年くらい後になるんでしょうかね? あまりその辺のことはよく分かりませんが、とりあえず「問題なく使っていいよね」と言えるようになるのはまだまだ先の話になりそうな気がします。

@scope の件も含めて Mozilla がんばれ。涙

そんな訳で

今回は Google Chrome v140 の新機能である CSS typed arithmetic(CSS 型付き算術)の簡単な説明と、その実践的なユースケースとして単位なしの line-height 値を動的に算出する方法を、これまでの CSS だとこうするしかなかったよね…という実装例との比較を交えつつ書いてみました。

Chrome のリリースノートでも少し触れられていますが、この他にも例えば、いわゆる Modular Scale の *** に使えそう(←今は伏せておく、後日書くかも)とか、タイポグラフィに応用する方法はいくつか思いつきました。詳しい方ならもっと色々と便利なアイデアが出てくるのかもしれません。

リリースノートの最初の方に出してある割には地味な機能だと感じられた方が(もしかすると)いるかもしれませんが、個人的にはそんなことなくて、けっこう便利に使っていけるんじゃないかなぁと思っております。

という訳で、みなさんも何か活用法を思いついたら自分のブログでも、あるいは Note でも Zenn でも Qiita でも何でもいいので何かに書いてストックしておくと、全主要ブラウザで問題なく使えるようになった時にきっと役に立つはずなので、どんどんアウトプットしてみてください。そしたらいいのがあった時にオレがパクれて(ry

  1. …と日本語版リリースノートでは訳されています。個人的にはどちらかというと「型」より「単位」の方が適切な気もしましたが、ここは公式に従って同じ訳語をそのまま使うことにします。 

  2. とても全部は書き切れないのでここでは省略しますが、もちろんその他の Chromium 採用ブラウザでも v140 ベースとなった時点で順次サポートが入るはずです。 

  3. そうはいっても許容されるのはデータの型が同一の単位に限られますので、(そもそもそんな計算をしたい人はいないと思いますが)calc(100px + 0.5s) のようなことはできません。 

  4. 実際にはこれに line-height をピクセル値→単位なしの値に変換する作業も入るのですが、とりあえず今は考えないことにします。