🔠

CSS カスタムプロパティによる流動的フォントサイズ

2022/01/21に公開

はじめに

画面幅に対して流動的にフォントサイズを変化させたい場合、vw などの単位を用いると実現できます。たとえば font-size: 5vw; を指定すると、幅 960px ではフォントサイズは 48px、幅 320px では 16px になります。しかし、320px16px は小さすぎるので 32px にしたいとなると、計算が必要になってきます。

この計算方法や流動的フォントサイズのさらなる詳細については、以前登壇したときの資料をご覧いただきたい 👇

https://speakerdeck.com/ixkaito/taipogurahuibesituku-plus-dezainaidea?slide=51

資料にもございますが、計算して CSS を出力してくれるアプリも作っています 👇

https://ixkaito.github.io/viewportscaler/

Sass の mixin もあります 👇

https://github.com/ixkaito/viewportscale

今回は、calc() clamp() min() max() と CSS カスタムプロパティを駆使して、自分で計算する必要がなく、Sass なども不要で CSS のみでの実装を紹介いたします。

使い方と実装解説

デモとソースコード

使い方

まずは下記コードを CSS に貼り付けてください。コメント部分はドキュメントなので任意ですが、使い方の参考になります。あとはコメント部分のドキュメントを参考し、任意の要素に必要な CSS カスタムプロパティを指定するだけです。

css
/**
* Available vars:
* @var --viewport-from: <number> - Number in pixels without the unit. Required if `--font-size` is not exist.
* @var --viewport-to: <number> - Number in pixels without the unit. Required if `--font-size` is not exist.
* @var --font-size-from: <number> - Number in pixels without the unit. Required if `--font-size` and `--min-font-size` is not exist.
* @var --font-size-to: <number> - Number in pixels without the unit. Required if `--font-size` and `--max-font-size` is not exist.
* @var --max-font-size: <number> - Number in pixels without the unit. Optional.
* @var --min-font-size: <number> - Number in pixels without the unit. Optional.
* @var --viewport-unit-converter: 1vw | 1vh | 1vmin | 1vmax - Optional. Default: 1vw.
* @var --font-size: <length> | <percentage> | <absolute-size> | <relative-size> | Global values - Optional.
*/
*,
*::before,
*::after {
  --viewport-unit-converter: 1vw;
  --fz-from: var(--font-size-from, var(--min-font-size));
  --fz-to: var(--font-size-to, var(--max-font-size));
  --fz-slope: (var(--fz-to) - var(--fz-from)) / (var(--viewport-to) - var(--viewport-from)) * 100;
  --fz-intercept: (var(--viewport-to) * var(--fz-from) - var(--viewport-from) * var(--fz-to)) / (var(--viewport-to) - var(--viewport-from));
  --font-size: calc(var(--fz-slope) * var(--viewport-unit-converter) + var(--fz-intercept) * 1px);

  --min-fz-px: calc(var(--min-font-size) * 1px);
  --max-fz-px: calc(var(--max-font-size) * 1px);
  --clamp: clamp(var(--min-fz-px), var(--font-size), var(--max-fz-px));
  --max: var(--has-max, var(--min));
  --min: var(--has-min, var(--font-size));
  --has-max: min(var(--max-fz-px), var(--font-size));
  --has-min: max(var(--min-fz-px), var(--font-size));

  font-size: var(--clamp, var(--max));
}

例 1:

css
h1 {
  --viewport-from: 320;
  --font-size-from: 32;
  --viewport-to: 960;
  --font-size-to: 48;
}

この場合、幅 320px では 32px960px では 48px になりますが、320px 以下や 960px 以上でも同じ変化率で小さくなったり大きくなったりします。

例 2:

css
h1 {
  --viewport-from: 320;
  --viewport-to: 960;
  --min-font-size: 32;
  --max-font-size: 48;
}

この場合は上記と同じ変化率ですが、32px 以下や 48px 以上にはならない。

css
h1 {
  --viewport-from: 320;
  --font-size-from: 32;
  --viewport-to: 960;
  --font-size-to: 48;
  --min-font-size: 32;
  --max-font-size: 48;
}

このように記述しても同じ結果になります。--font-size-from--min-font-size--font-size-to--max-font-size がそれぞれ同じ場合は、--font-size-from--font-size-to は省略できます。

例 3:

css
h1 {
  --viewport-from: 320;
  --viewport-to: 960;
  --min-font-size: 32;
  --font-size-to: 48;
}

このように最小だけ、最大だけを指定することもできます。

例 4:

css
h1 {
  --viewport-from: 320;
  --font-size-from: 32;
  --viewport-to: 960;
  --font-size-to: 48;
  --min-font-size: 36;
  --max-font-size: 40;
}

--font-size-from--min-font-size--font-size-to--max-font-size がそれぞれ違う値でも大丈です。この場合幅 320px では 32px960px では 48px になるように変化させるが、その途中の 36px40px で変化は止まります。

例 5:

css
h1 {
  --viewport-from: 320;
  --font-size-from: 32;
  --viewport-to: 960;
  --font-size-to: 48;
}

@media (min-width: 960px) {
  h1 {
    --viewport-from: 960;
    --font-size-from: 48;
    --viewport-to: 1920;
    --font-size-to: 64;
  }
}

メディアクエリを使えば途中で変化率を変えられます。上記の例はメディアクエリ部分がなければ 1920px では 72px になるはずですが、960px から変化率変えて 1920px では 64px になります。

例 6:

css
h1 {
  --viewport-from: 320;
  --font-size-from: 48;
  --viewport-to: 960;
  --font-size-to: 32;
  --min-font-size: 16;
}

逆に大きいサイズから小さいサイズに変化させることもできます。最大最小値とも組み合わせられます。

例 7:

css
h1 {
  --font-size: 2rem;
}

固定値も指定できます。--font-size が指定してあれば、その他プロパティは不要です。px 以外の font-size で指定できる値もすべて指定できます。

例 8:

css
h1 {
  --font-size: calc(2.5vw + 24px);
}

独自の計算式を指定することもできます。

例 9:

css
h1 {
  --font-size: calc(2.5vw + 24px);
  --min-font-size: 32;
  --max-font-size: 48;
}

独自計算式に最大最小値を設定することもできます。

例 10:

css
h1 {
  --viewport-unit-converter: 1vh;
  --viewport-from: 320;
  --font-size-from: 32;
  --viewport-to: 960;
  --font-size-to: 48;
}

デフォルトでは vw を用いて画面幅に対して計算しますが、vh vmin vmax にもできます。注意として 1vh のように 1 をつける必要があります。

Tailwind CSS での使い方

Tailwind CSS の @tailwind base には Preflight (CSS ノーマライズ・リセットみたいなもの) が含まれています (tailwind.config.js で無効化できます) 。その中には h1, h2, h3, h4, h5, h6font-size に関する記述があるため、CSS ファイルには以下のように記述する必要があります。

css
@tailwind base;
@tailwind components;
@tailwind utilities;

/**
* Available vars:
* @var --viewport-from: <number> - Number in pixels without the unit. Required if `--font-size` is not exist.
* @var --viewport-to: <number> - Number in pixels without the unit. Required if `--font-size` is not exist.
* @var --font-size-from: <number> - Number in pixels without the unit. Required if `--font-size` and `--min-font-size` is not exist.
* @var --font-size-to: <number> - Number in pixels without the unit. Required if `--font-size` and `--max-font-size` is not exist.
* @var --max-font-size: <number> - Number in pixels without the unit. Optional.
* @var --min-font-size: <number> - Number in pixels without the unit. Optional.
* @var --viewport-unit-converter: 1vw | 1vh | 1vmin | 1vmax - Optional. Default: 1vw.
* @var --font-size: <length> | <percentage> | <absolute-size> | <relative-size> | Global values - Optional.
*/
*,
*::before,
*::after,
h1,
h2,
h3,
h4,
h5,
h6 {
  --viewport-unit-converter: 1vw;
  --fz-from: var(--font-size-from, var(--min-font-size));
  --fz-to: var(--font-size-to, var(--max-font-size));
  --fz-slope: (var(--fz-to) - var(--fz-from)) / (var(--viewport-to) - var(--viewport-from)) * 100;
  --fz-intercept: (var(--viewport-to) * var(--fz-from) - var(--viewport-from) * var(--fz-to)) / (var(--viewport-to) - var(--viewport-from));
  --font-size: calc(var(--fz-slope) * var(--viewport-unit-converter) + var(--fz-intercept) * 1px);

  --min-fz-px: calc(var(--min-font-size) * 1px);
  --max-fz-px: calc(var(--max-font-size) * 1px);
  --clamp: clamp(var(--min-fz-px), var(--font-size), var(--max-fz-px));
  --max: var(--has-max, var(--min));
  --min: var(--has-min, var(--font-size));
  --has-max: min(var(--max-fz-px), var(--font-size));
  --has-min: max(var(--min-fz-px), var(--font-size));

  font-size: var(--clamp, var(--max));
}

あとは arbitrary values (「任意値」、これまでは JIT モードとして知られている) でカスタムプロパティを指定するだけです。上記の「例 2」は以下のように記述します。

<h1 class="[--viewport-from:320] [--viewport-to:960] [--min-font-size:32] [--max-font-size:48]">
  ...
</h1>

以上で使い方は大体網羅できたかと思います。

解説

計算式はこれまで作ってきたものと同じなので割愛いたします。

ポイント 1

CSS カスタムプロパティの初期値や計算式を :root にしてしまうと、値の継承で期待した結果にならないため、* を使ってすべての要素に適用させています。

ポイント 2

CSS の calc() は単位がついた値の加算減算はできますが、乗算除算はできません。なので単位がついていない値で計算したあとに、単位をつけてあげる必要があります。そのため、ほとんどのプロパティは単位なしの数値で指定する必要があります。

単位をつける方法は var(--fz-intercept) * 1px のように単位のついた 1 を掛けることで実現できます。例 10 で 1vh にしないといけないのはこのためです。

ポイント 3

最大最小値のあるなしの条件分岐については、clamp() min() max() は正しく引数を渡さないと、その全体が正しくない値になり、それを CSS カスタムプロパティのフォールバックと組み合わせると、正しくない場合はフォールバックを参照することになり、それをバケツリレー形式でつなげて実現しています。

ソースコードや以前のツイートもご参考ください。

https://twitter.com/ixkaito/status/1483358897931010058

アクセシビリティ

こちらは px を基に計算していますが、rem に対応させることもできなくはないです。ただ、そうなると計算式がもっと複雑になり、さらに、ブラウザのデフォルトフォントサイズを変更している場合、期待した数値にはならなくなります。では逆にアクセシビリティ的にどうなの?ということですが、これは前々から主張していることですが、px でコーディングされていても、ブラウザ側が文字サイズのみの拡大縮小を可能にするべきです。実際、Firefox と Safari はそれが可能です。もちろん今回の実装にも問題なく対応しています。

https://twitter.com/ixkaito/status/1480031949758820356

おわりに

別の研究で生まれた副産物ですが、意外と結構使えるものができたと思います。それではよい CSS ライフを。

Discussion