テクニック

font-size を clamp() でスマホ対応に|PCまで1行でつなぐ

この記事の対象: メディアクエリで文字サイズを切り替えていて、段の数が増えすぎたWeb制作者
読了時間: 約12分

font-size をメディアクエリで3段4段に切り替えると、ブレークポイントの境目で文字が飛びます。スマホの375pxで読めるサイズを決め、タブレットで一度調整し、PCでまた調整する。そのうち「幅768pxでだけ見出しが大きすぎる」という指摘が来ます。clamp() は最小値・推奨値・最大値の3つを1行で書き、画面幅に合わせて連続的に変える関数です。段がなくなるので境目の飛びも消えます。ただし書き方を間違えると、ブラウザの文字サイズ設定を無視する実装になり、アクセシビリティ上の問題になります。この記事では式の立て方と、rem を混ぜる理由を扱います。


clamp() で font-size を決めるとは何をすることか

clamp() で font-size を決めるとは何をすることか

3つの値の役割

clamp() は引数を3つ取ります。

font-size: clamp(1.5rem, 1.125rem + 1.5vw, 2.5rem);

左から最小値・推奨値・最大値です。ブラウザは推奨値を計算し、それが最小値を下回れば最小値を、最大値を上回れば最大値を使います。真ん中の推奨値に vw(ビューポート幅の1%)が入っているので、画面幅が広がるほど値が増えていきます。

引数 役割 決め方
第1引数(最小値) いちばん狭い画面での文字サイズ 本文16px以上、見出しは本文の1.4倍以上
第2引数(推奨値) 幅に応じて変わる値 rem + vw の形で式から算出する
第3引数(最大値) いちばん広い画面での文字サイズ デザインデータの最大幅での指定値

ポイントは第2引数だけが可変だということです。第1引数と第3引数は固定値で書きます。ここに vw を入れると上限と下限が画面幅で動いてしまい、クランプの意味がなくなります。

メディアクエリとの違い

段で切り替える場合と連続で変える場合

clamp() で連続に変える

  • 宣言は1行、上書きが起きない
  • 境目で文字が飛ばない
  • 幅767pxと768pxの差が1px未満
  • 最小と最大を自分で決められる

メディアクエリで段を切る

  • 段ごとに宣言が増える
  • 境目で数px〜十数px飛ぶ
  • 段の間の幅は検証されないまま
  • 要素が増えると宣言が掛け算で増える

メディアクエリで段を切る方式は、段の数だけ検証する幅が増えます。見出し5種類×ブレークポイント3つで15行の宣言になり、どれかを直したとき他の段と矛盾していないか確認する作業が毎回つきます。clamp() は1行なので、確認するのは最小値と最大値の2点だけです。


推奨値の vw をどう計算するか

推奨値の vw をどう計算するか

2点を決めてから式を作る

やり方は決まっています。「この幅でこのサイズ」という2点を決め、その2点を通る直線の式を立てるだけです。

clamp() の式を作る手順
2点を決める最小幅・最大幅と、それぞれでの文字サイズ
傾きを出すサイズの差を幅の差で割る
切片を出す片方の点から傾き×幅を引く
単位を付ける傾きは×100でvw、切片はrem換算

4つ目の「単位を付ける」が唯一ひねりのある工程です。傾きは「px / px」の無次元数なので、100倍して vw にします。

具体的に計算します。前提は次の通りです。

  • 最小幅 375px で見出しを 24px にする
  • 最大幅 1280px で見出しを 40px にする
  • ルートの font-size は初期値の 16px のまま

傾き

(40 - 24) ÷ (1280 - 375) = 16 ÷ 905 = 0.017679...

vw に変換

0.017679 × 100 = 1.7679vw → 1.77vw

切片(375px の点を使う)

24 - (0.017679 × 375) = 24 - 6.6298 = 17.3702px

切片を rem に

17.3702 ÷ 16 = 1.0856rem → 1.086rem

できた式がこれです。

h2 {
  font-size: clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem);
}

最小値 1.5rem は 24px、最大値 2.5rem は 40px です。幅375pxでは 1.086rem + 1.77vw = 17.37 + 6.64 = 24.01px。幅1280pxでは 17.37 + 22.66 = 40.03px。狙った2点をほぼ通っています。小数の丸めで0.03pxずれますが、画面上は区別できません。

検算を省かない

式を手で立てると符号と桁を間違えます。計算したら必ず2点に代入して確認します。ブラウザのコンソールで済みます。

// clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem) を検算する
const root = 16;                 // ルートの font-size
const slope = 1.77 / 100;        // vw を比率に戻す
const intercept = 1.086 * root;  // rem を px に

[320, 375, 768, 1024, 1280, 1920].forEach((w) => {
  const pref = intercept + slope * w;
  const size = Math.min(Math.max(pref, 1.5 * root), 2.5 * root);
  console.log(`${w}px → 推奨 ${pref.toFixed(2)}px / 実際 ${size.toFixed(2)}px`);
});

出力を見ると、320pxでは推奨値が23.03pxで最小値24pxに切り上がり、1920pxでは推奨値が51.37pxで最大値40pxに切り下がることがわかります。つまり320px以下と1280px以上は固定サイズになる。これが意図通りかどうかを、コードを書いた時点で確かめます。

式を書かずに済ませる書き方

毎回計算するのが面倒なら、CSSの calc() に計算そのものを書けます。読み手が意図を追えるので、保守する人が自分以外のときはこちらを推します。

:root {
  /* 設計の2点をここだけで管理する */
  --vw-min: 375;
  --vw-max: 1280;
}

h2 {
  --size-min: 24;  /* px */
  --size-max: 40;  /* px */

  /* 傾き×100vw + 切片 を計算式のまま書く */
  font-size: clamp(
    calc(var(--size-min) / 16 * 1rem),
    calc(
      (var(--size-min) / 16 * 1rem)
      + (var(--size-max) - var(--size-min))
      * (100vw - (var(--vw-min) * 1px))
      / (var(--vw-max) - var(--vw-min))
    ),
    calc(var(--size-max) / 16 * 1rem)
  );
}

数値を変えるのは --size-min と --size-max の2行だけになります。デザインデータの数字をそのまま書き写せるので、転記ミスが減ります。式の中に rem と px と vw が混ざっていますが、calc() の中では単位の異なる長さ同士の加減算ができるので成立します。


rem を混ぜる理由とアクセシビリティへの影響

rem を混ぜる理由とアクセシビリティへの影響

vw だけで書くと文字サイズ設定が効かなくなる

ここがこの記法でいちばん事故が起きる箇所です。

/* これはやってはいけない */
h2 {
  font-size: clamp(24px, 3.1vw, 40px);
}

vw はビューポート幅だけで決まり、ユーザーがブラウザの文字サイズを大きくしても変わりません。px の最小値・最大値も同じです。つまりこの1行は、ブラウザの「文字の大きさ」設定を完全に無視します。弱視の人や、加齢で小さい文字が読みにくくなった人が設定を120%にしても、見出しのサイズは1pxも動きません。

WCAG 2.2 の達成基準 1.4.4「テキストのサイズ変更」は、支援技術なしで200%まで拡大できても内容と機能が損なわれないことを求めています。ブラウザのズーム(ページ全体の拡大)は vw も一緒に拡大されるので通りますが、文字サイズのみの設定変更には応えません。この2つは別物です。

対策は、最小値・最大値・切片をすべて rem で書き、vw は傾きの部分だけにすることです。

/* ルート相対を混ぜる。文字サイズ設定が効く */
h2 {
  font-size: clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem);
}

ユーザーがルートの文字サイズを16px→20pxに上げると、1.5rem は30px、切片の 1.086rem は21.7pxになり、全体が連動して大きくなります。vw の分だけは動きませんが、サイズの大半は rem 側が担っているので実用上の拡大は効きます。

rem の比率をどう置くか

vw の比率が大きいほど画面幅への追従が強く、文字サイズ設定への応答が弱くなります。目安はこうです。

用途 vw の寄与 書き方の目安
本文・リード文 小さく(0〜0.3vw) ほぼ rem 固定。可変させない選択もある
小見出し(h3・h4) 中(0.3〜0.8vw) 最小と最大の差を4px程度に抑える
大見出し(h1・h2) 大(1〜3vw) 最小と最大の差を12〜20px取る
装飾的な巨大文字 非常に大(4vw以上) 読解に不要な文字に限る

本文は clamp() を使わず固定値にしてよい、というのが実務での判断です。本文のサイズは「読める大きさ」で決まり、画面が広いから大きくする理由はありません。スマホで16px、PCで17pxという程度の差を付けるなら clamp() の価値はあるものの、1pxのために式を増やすかは案件次第です。見出しのように最小と最大で1.5倍以上変わる要素のほうが効きます。

ルートに clamp() を置くのは避ける

html { font-size: clamp(...) } と書いて、あとは全部 rem で組むという作り方を見かけます。これはやらないほうがいいと考えています。理由は2つです。

  • ルートの文字サイズを変えると、rem で書いたすべての余白・幅・角丸が一緒に動く。文字だけ大きくしたいのに、レイアウト全体が拡大する
  • ユーザーがブラウザで設定した文字サイズを、CSSが上書きしてしまう。16px 相当の値を書けば、設定が20pxでも16pxに戻る

ルートは触らず、要素ごとに clamp() を書きます。宣言は増えますが、何が動いて何が動かないかが読めます。

行間・字間も合わせて見る

文字サイズだけ可変にして line-height を px で固定すると、大きい側で行が詰まります。line-height は単位なしの数値で書きます。

h2 {
  font-size: clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem);
  line-height: 1.4;          /* 単位なし。font-size に連動する */
  letter-spacing: 0.02em;    /* em。font-size に連動する */
  text-wrap: balance;        /* 見出しの行末を揃える。対応は下の節を参照 */
}

letter-spacing を px で書くと、小さい側でだけ字間が広く見えます。em にすれば比率で追従します。日本語の見出しは字間を詰めすぎると読みにくくなるので、0.02em〜0.05em 程度から調整します。

コントラストはサイズと別の話ですが、文字を小さくする方向で clamp() を使うときだけ関係します。WCAG 2.2 の基準では、18pt(24px)以上または14pt(18.66px)以上の太字を「大きいテキスト」として扱い、コントラスト比の要求が4.5:1から3:1に下がります。clamp() の最小値が24pxを下回る設計にすると、狭い画面でだけこの境界をまたぐことがあります。薄いグレーの見出しを可変にするなら、最小値側のサイズでコントラストを測り直します。


表示速度とレイアウトへの影響

表示速度とレイアウトへの影響

再計算は起きるが、重い処理ではない

clamp() の計算自体はブラウザのスタイル計算の一部で、JavaScript のリスナーでリサイズを監視する実装より軽いです。かつて「fluid typography」を実現するために resize イベントで font-size を書き換える手法がありましたが、あれは再描画が毎フレーム走ります。clamp() に置き換えればそのコードは全部消せます。

ただし vw が入っている以上、ビューポート幅が変わるたびにレイアウトが再計算されます。影響が出るのは次の場面です。

  • スマホでアドレスバーが隠れる動きに連動して幅が変わる端末(高さは dvh で扱うが、幅も微妙に動くことがある)
  • 縦スクロールバーが出入りするページ。スクロールバーの幅ぶん 100vw と実際の表示幅がずれる
  • clamp() を数百要素に当てたリストや表

3つ目は実測で確認します。Chrome の DevTools の Performance パネルでリサイズ中のプロファイルを取り、Recalculate Style と Layout の時間を見ます。要素数が多いテーブルで気になるなら、セル個別ではなく親に font-size を当てて子を em で相対指定します。宣言の数が減ります。

スクロールバーのずれを踏む

100vw はスクロールバーを含んだ幅です。デスクトップでスクロールバーが常時表示される環境では、実際のコンテンツ幅より 100vw が15px前後大きくなります。文字サイズの計算では1px未満の差なので実害は出ませんが、同じ vw を幅や余白にも使っていると横スクロールが発生します。文字サイズに使うだけなら気にしなくてよく、レイアウトに使うなら別の手段を取ります。

親要素の幅を基準にしたいなら、コンテナクエリの単位(cqw など)が選択肢になります。サイドバーの中とメインカラムの中で同じコンポーネントのサイズを変えたい、という要件はこちらの領分です。

Webフォントと併用するとき

clamp() でサイズを可変にしていると、Webフォントの読み込み前後での見た目の変化が目立ちます。代替フォントとWebフォントで文字幅が違い、サイズも可変なので、行数が変わって要素の高さが動きます。font-display: swap を使うなら size-adjust や ascent-override で代替フォントのメトリクスを寄せておくと揺れが減ります。日本語フォントは容量が大きいので、この問題はサイズの可変よりも先に手を打つ対象です。

prefers-reduced-motion との関係

clamp() はアニメーションではありません。リサイズに応じて値が変わるだけで、時間経過で動くものではないので prefers-reduced-motion の対象外です。ユーザーがウィンドウ幅を変えなければ文字サイズも変わりません。

ただし、font-size に transition を掛けている場合は話が別です。

/* 動きを減らす設定を尊重する */
h2 {
  font-size: clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem);
  transition: font-size 200ms ease;
}

@media (prefers-reduced-motion: reduce) {
  h2 {
    transition: none;
  }
}

そもそも font-size に transition を掛ける必要はほぼありません。リサイズ中にじわじわ文字が追いかけてくる挙動は、気持ちよさよりも読みにくさが勝ちます。掛けないのが既定だと考えてください。

キーボード操作への影響もありません。フォーカスリングは outline で描かれ、outline-offset を em で指定していれば文字サイズに追従します。px 固定にしていると大きい文字でリングが文字に食い込むので、em にしておきます。


案件ごとに使うか使わないか

「便利だからどこでも使う」ではなく、案件の性質で判断します。

使う

コーポレートサイト・ブランドサイトのキービジュアルと大見出し
最小と最大でサイズが1.5倍以上変わる要素です。段で切り替えると境目が目立ちます。clamp() の効果がもっとも出る場所です。

1カラムで縦に読ませる構成
横幅が広いときに文字を大きくしないと、1行が長くなりすぎて読みにくくなります。文字サイズと max-width の両方で調整します。

デザインデータがPCとスマホの2種類しかない案件
中間の幅を誰も設計していない状態です。段で切るとその中間が未検証のまま公開されます。clamp() なら2点を結ぶので、間が自動で埋まります。

使わない

本文・キャプション・フォームのラベル
固定値で足ります。読める大きさは画面幅に依存しません。16px(1rem)を下回らないことだけ守ります。特にフォームの入力欄は、iOSのSafariで16px未満だとフォーカス時にページが自動ズームします。これはユーザー体験を壊すので、input と select と textarea は font-size: 1rem 以上を固定します。

管理画面・業務システム
使う人の画面幅が決まっていて、情報密度が優先されます。幅を広げたら文字が大きくなる挙動は、一覧表示を見るときに邪魔になります。固定サイズ+ユーザーが選べる表示密度の切り替えのほうが合います。

数字を縦に揃えて読む表
サイズが可変だと桁位置の確認がしにくくなります。font-variant-numeric: tabular-nums と固定サイズの組み合わせが向きます。金融系・証券系のサイトで数値を見せる画面は特にそうです。

既存サイトの部分改修
既存のCSSがメディアクエリで段を切っているなら、一部だけ clamp() にすると2つの方式が混在します。見出しだけ連続で変わり、本文は段で切り替わるので、幅を変えたときに相対関係が崩れます。改修するならタイポグラフィ一式をまとめて置き換えるか、既存方式のままにします。


そのまま使えるタイポグラフィ一式

ここまでの判断を1枚のCSSにまとめます。コピーして数値を差し替えれば動きます。

/* ------------------------------------------------------------------
   可変タイポグラフィ
   設計の2点: 375px / 1280px
   ルートの font-size は触らない(ユーザー設定を尊重する)
------------------------------------------------------------------ */

:root {
  /* 見出し: 最小 / 切片rem + 傾きvw / 最大 */
  --fs-display: clamp(2rem, 1.227rem + 3.31vw, 4rem);   /* 32 → 64px */
  --fs-h1:      clamp(1.75rem, 1.293rem + 1.96vw, 2.9rem); /* 28 → 46.4px */
  --fs-h2:      clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem);  /* 24 → 40px */
  --fs-h3:      clamp(1.25rem, 1.112rem + 0.59vw, 1.5rem); /* 20 → 24px */

  /* 本文系は固定。画面幅で変える理由がない */
  --fs-body:    1rem;      /* 16px */
  --fs-lead:    1.125rem;  /* 18px */
  --fs-small:   0.875rem;  /* 14px */

  --lh-tight: 1.3;
  --lh-normal: 1.8;
}

body {
  font-size: var(--fs-body);
  line-height: var(--lh-normal);
  /* 日本語の改行位置は書き手が決める */
  word-break: keep-all;
  overflow-wrap: anywhere;
}

.display {
  font-size: var(--fs-display);
  line-height: var(--lh-tight);
  letter-spacing: 0.01em;
  font-weight: 700;
}

h1 {
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  letter-spacing: 0.02em;
}

h2 {
  font-size: var(--fs-h2);
  line-height: 1.4;
  letter-spacing: 0.02em;
}

h3 {
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: 0.02em;
}

.lead {
  font-size: var(--fs-lead);
  line-height: 1.9;
  max-width: 34em;
}

.caption,
small {
  font-size: var(--fs-small);
  line-height: 1.7;
}

/* フォーム要素は16px未満にしない(iOS Safari の自動ズーム対策) */
input,
select,
textarea,
button {
  font-size: 1rem;
  font-family: inherit;
}

/* フォーカスリングは em で文字サイズに追従させる */
:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 0.15em;
}

/* 文字サイズに transition は掛けない。掛けるなら動きを減らす設定を見る */
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

--fs-display の式を検算すると、375pxで 19.63 + 12.41 = 32.04px、1280pxで 19.63 + 42.37 = 62.0px。最大値 4rem(64px)には1468px付近で到達します。最大幅を1280pxで設計したつもりが、実際には1468pxまで伸び続ける。丸めの影響で2点のうち片方がずれるのは珍しくないので、両端で検算する習慣が必要です。

単位なしの line-height を使う理由

line-height: 1.4 のように単位を書かない指定は、その要素の font-size を基準に計算されます。font-size が可変なら行間も一緒に動きます。line-height: 32px と書くと、文字が40pxになっても行間は32pxのままで文字が重なります。1.4em でも計算値は追従しますが、子要素には計算後の px が継承されるので、親子で文字サイズが違うときに意図しない結果になります。単位なしが安全です。


ブラウザ対応と確認のしかた

ブラウザ対応と確認のしかた

clamp() は CSS Values and Units Module Level 4 の関数で、主要ブラウザに実装されています。Chrome・Edge・Safari・Firefox の現行版では動きます。正確な対応バージョンは記憶で書かず、案件ごとにCan I useで引き直してください。

確認の手順はこうします。

  1. Can I use で clamp() と css-math-functions を引く。グローバルと日本のシェアを両方見る
  2. GA4で対象サイトの実際のブラウザ構成を見る。レポート → ユーザー → テクノロジー → 「ブラウザ」と「ブラウザのバージョン」。母集団の少ないバージョンに合わせて設計を縛らない
  3. フォールバックが要るか判断する。要るなら clamp() の前に固定値を書く
h2 {
  font-size: 1.75rem;                                 /* 未対応時に使われる */
  font-size: clamp(1.5rem, 1.086rem + 1.77vw, 2.5rem); /* 対応時はこちらが勝つ */
}

clamp() を解釈できないブラウザは2行目を無効な宣言として捨て、1行目が残ります。追加の分岐も @supports も不要です。ただし現時点でこのフォールバックが必要になる環境はかなり限られます。要件に古いブラウザへの対応が入っている案件でだけ書く、という扱いで足ります。

text-wrap: balance はこれより新しい機能で、対応状況が別です。clamp() と同じタイミングで入れるなら、こちらは別途Can I useで確認します。未対応でも通常の折り返しになるだけなので、フォールバックは不要です。

実機で何を見るか

数値の検算が通っても、実機で見ないと判断できないことがあります。

確認する幅 見るもの
320px 最小値が効いているか。見出しが3行以上に折れていないか
375px 設計した最小側の2点目。デザインデータと一致するか
768px 中間。段で切っていた頃に問題が出ていた幅
1280px 設計した最大側の2点目
1920px 最大値で止まっているか。間延びしていないか

加えてブラウザの文字サイズ設定を「大」にして全部もう一度見ます。Chrome なら設定 → デザイン → フォントサイズ。vw だけで書いた箇所があれば、ここで動かないのが見つかります。

日本語の見出しは、サイズが変わると折り返し位置が変わります。意味の切れ目でない場所で折れていないか、PCとスマホの両方で目で読んで確認します。折れ位置を制御したいなら <wbr> を意味の切れ目に置きます。


よくある質問

clamp() の第2引数に vw だけを書いてはいけないのですか

書けますが推奨しません。font-size: clamp(1.5rem, 3.1vw, 2.5rem) のように vw 単独にすると、ブラウザの文字サイズ設定を変えても推奨値が動かず、最小値と最大値の rem だけが反応します。結果として設定を大きくした人には、ほぼ常に最小値か最大値が適用される挙動になります。切片を rem で持たせて rem + vw の形にしてください。

本文の font-size も clamp() にするべきですか

案件によりますが、固定値で足りることが多いです。本文の読みやすさは画面幅ではなく文字の絶対サイズと1行の文字数で決まるので、幅が広いときは max-width を em で絞るほうが効きます。16pxと17pxの差を付けるために式を増やすかは、保守する人の人数で決めてください。

ルートの font-size に clamp() を当てて全部 rem で組むのはどうですか

避けたほうがいいと考えています。ルートを変えると rem で書いた余白・幅・角丸がすべて連動し、文字だけ大きくするつもりがレイアウト全体の拡大になります。さらにルートに具体的な値を書くこと自体が、ユーザーのブラウザ設定を上書きする行為です。要素ごとに clamp() を書くほうが、何が動くかを把握できます。

メディアクエリと clamp() を併用してもいいですか

できますが、同じ要素に両方を当てるのは混乱の元です。たとえば縦書きと横書きを切り替えるような、サイズ以外の要素がまとめて変わる場面ではメディアクエリが必要になります。その場合はメディアクエリの中で clamp() の数値を差し替える形にして、「サイズを連続で変えるのは clamp、構造を切り替えるのはメディアクエリ」と役割を分けてください。

式を間違えていないかを手早く確認する方法はありますか

DevTools の Elements パネルで要素を選び、Computed タブの font-size を見ます。ウィンドウ幅を変えながら数値が意図通りに動くかを追えます。両端の幅(設計した最小と最大)で、デザインデータの指定値と一致しているかを必ず見てください。記事中のJavaScriptスニペットをコンソールに貼れば、複数の幅を一度に計算できます。


まとめ

  • clamp(最小, 推奨, 最大) の第2引数だけが可変。第1引数と第3引数は固定値で書く
  • 推奨値は「この幅でこのサイズ」の2点から直線の式を立てる。傾きは×100で vw、切片は rem 換算
  • 最小値・最大値・切片をすべて rem にする。px と vw だけで書くとブラウザの文字サイズ設定が効かなくなる
  • 式を書いたら両端の幅で検算する。丸めで片方がずれることがある
  • line-height は単位なし、letter-spacing は em。font-size に transition は掛けない
  • 本文・フォーム・業務システムの表は固定サイズ。見出しとキービジュアルに絞って使う
  • ブラウザ対応はバージョン番号の暗記ではなく、Can I use と GA4 の実データで案件ごとに確認する

段を切る方式をやめると、検証する幅は「設計した2点」と「両端で止まっているか」の4点に減ります。確認する項目が減ることが、この書き方のいちばんの利点です。