デザインから二つの条件を渡されたとします。見出しは幅 320px の画面で 16px、幅 1440px の画面で 32px。意図は明快ですが、実装方法は自明ではありません。メディアクエリで二つのサイズを切り替えることも、clamp(1rem, 2.5vw, 2rem) のような値を手書きすることもできます。しかし、もっともらしい clamp() が二つのデザイン目標を正確に通るとは限りません。
正確な流体サイズに勘は不要です。二組の「ビューポート幅と値」があれば直線が一つ決まり、その直線を固定境界付きの CSS ビューポート式に変換できます。
この記事では、式の導出と検算、増加・減少する範囲、rem と px の選び方を解説し、JavaScript、Python、Bash の小さな生成コードも紹介します。目的は計算機から謎の数値をコピーすることではなく、コードレビューで根拠を説明できる CSS を作ることです。
clamp() が実際に行うこと
CSS の clamp() は、カンマで区切った三つの計算を受け取ります。
property: clamp(minimum, preferred, maximum);
ブラウザーは中央の推奨式を評価し、その結果を下限と上限の間に収めます。形式的には次と同じです。
max(minimum, min(preferred, maximum))
この定義は重要です。中央の式は「既定値」ではなく、現在のビューポートでブラウザーが評価するレスポンシブな規則です。その結果が許容範囲を外れたときだけ境界が使われます。
たとえば、次の宣言を考えます。
font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
動作は三つの領域に分けられます。
- 狭い画面では推奨値が
1rem未満になるため、最小値が使われます。 - 選択した二つの端点の間では、中央の式がビューポートに応じて線形に変化します。
- 広い画面では推奨値が
2remを超えるため、最大値が使われます。
MDN の clamp() ドキュメントでは、現代のブラウザーで広く利用できる機能として掲載されています。比較関数の正式な動作は CSS Values and Units Level 4 で定義されています。
clamp(1rem, 2.5vw, 2rem) だけでは不十分な理由
単純なビューポート項が役立つこともありますが、そこにあるのは傾きだけです。切片を独立して指定できないため、任意の二つのデザイン目標を同時に通ることは通常できません。
よく見かける次の宣言を確認してみます。
font-size: clamp(1rem, 2.5vw, 2rem);
320px のビューポートでは 2.5vw は 8px なので、最小値によって 16px になります。1440px では 2.5vw は 36px なので、最大値によって 32px になります。最終的に両方の境界には到達しますが、流体に変化する区間は 320px〜1440px ではなく、640px〜1280px です。880px では 22px になりますが、二つのデザイン端点を直線補間すれば 24px になるはずです。
不足しているのは切片です。両端点を正確に通るには、推奨式に固定項とビューポート項が必要になるのが一般的です。
fixed-intercept + viewport-slope
これは直線の式 y = mx + b そのものです。
二つの端点から式を導く
まず、四つの入力を定義します。
v₁:最小ビューポート幅v₂:最大ビューポート幅s₁:最小ビューポートでの値s₂:最大ビューポートでの値
傾きは、値の変化量をビューポートの変化量で割って求めます。
m = (s₂ - s₁) / (v₂ - v₁)
切片は、その直線を幅ゼロのビューポートまで延長したときの値です。
b = s₁ - m × v₁
CSS の 1vw はビューポート幅の 1% なので、1px あたりの傾きに 100 を掛けると vw 係数になります。
preferred = b + (m × 100)vw
計算例:320px で 16px、1440px で 32px
最初に傾きを計算します。
m = (32 - 16) / (1440 - 320)
= 16 / 1120
= 0.0142857143
次に切片を求めます。
b = 16 - 0.0142857143 × 320
= 11.4285714px
傾きを vw に変換します。
m × 100 = 1.42857143
したがって、px での結果は次のとおりです。
font-size: clamp(16px, 11.4286px + 1.4286vw, 32px);
ルートフォントサイズが 16px なら、固定長を 16 で割ります。vw 係数は変わりません。
font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
二つの端点を検算する
320px では、1vw は 3.2px です。
11.4286px + 1.4286 × 3.2px ≈ 16px
1440px では、1vw は 14.4px です。
11.4286px + 1.4286 × 14.4px ≈ 32px
この端点チェックだけで、手計算の間違いの大半を見つけられます。また、丸めを最後に行うべき理由も分かります。切片を求める前に傾きを丸めると、誤差が積み重なり、端点が目に見えてずれることがあります。
実用的な計算機の使い方
ZeroTool CSS clamp() 計算機の入力項目は、デザインの引き渡し内容に近い形になっています。
font-size、padding、margin、gap、width、またはカスタムプロパティを選びます。- 最小・最大ビューポート幅をピクセルで入力します。
- それぞれのビューポート端点で必要な値を入力します。
- px または rem を選びます。rem の場合はルートフォントサイズも確認します。
- 五つのサンプル値を確認し、プレビュースライダーを動かします。
- CSS 宣言全体、または生成された値だけをコピーします。
二点から決まるのは一本の直線なので、カーブは意図的に線形です。サンプル表には中点と四分点も表示されるため、数式としては正しくてもデザインとして不自然な範囲を見つけやすくなります。
この計算機は、単純なコード片では見落とされやすい三つのケースにも対応します。
- **逆方向の範囲:**ビューポートが広がるほど値が小さくなります。
- **固定範囲:**両端の値が同じなら、一つの固定長に簡略化します。
- **負の値:**負の長さに意味がある margin とカスタムプロパティで受け付けます。
計算と状態保存はすべてブラウザー内で行われます。リセットすると既定値に戻り、保存されたフォーム状態も削除されます。
rem と px の選び方
式の中で異なる単位を組み合わせても問題ありません。これは回避策ではなく、CSS の数学関数が互換性のある長さを組み合わせるための正式な仕組みです。
文字とユーザー設定に追従するシステムには rem
rem を選ぶと、最小値、最大値、固定切片がルートフォントサイズに対する相対値になります。
.hero-title {
font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
}
これにより、式の一部はユーザーのフォントサイズ設定との関係を保てます。タイポグラフィや、文字体系と一緒に拡大すべきスペーシングトークンには、通常 rem がよい出発点です。
ただし、変換には明示的なルートサイズが必要です。プロジェクトが html { font-size: 62.5%; } を指定しているのに、計算機を 16px のまま使えば、そのコードベースに合わない rem が生成されます。計算機のルート入力を、実際のデザインシステムが前提とする値に合わせてください。
端点が本当に固定寸法なら px
正確な CSS ピクセルの端点に合わせたい視覚寸法には、px 出力が役立ちます。
.content-shell {
padding-inline: clamp(20px, 12px + 2.5vw, 48px);
}
CSS ピクセルは物理デバイスのピクセルではありませんが、CSS レイアウト上では固定長です。文字に使う場合、ユーザーのフォント設定との関係は rem より弱くなりやすい点に注意してください。
vw 項は常にビューポート相対
rem を選んでも、式全体がフォント相対になるわけではありません。中央の式には vw が残り、ビューポート幅に反応します。固定項とビューポート相対項は、すべてのユーザー設定に同じようには反応しません。そのため、ズームと文字サイズ変更のテストが必要です。
端点の換算だけを確認したい場合は、CSS 単位変換ツールも利用できます。
逆方向の流体スケール
広い画面ほど値を小さくしたい場合もあります。コンパクトなモバイルカードではタッチしやすいように垂直パディングを増やし、高密度なデスクトップレイアウトでは減らす、といったケースです。320px で 32px、1440px で 16px を指定すると、傾きは負になります。
padding-block: clamp(1rem, 2.2857rem - 1.4286vw, 2rem);
小さいビューポートでは推奨式が 2rem、大きいビューポートでは 1rem になります。
ここには重要な実装上の注意点があります。CSS では引き続き下限を先に、上限を後に置く必要があります。スケールの方向が逆だからといって、clamp(2rem, ..., 1rem) と出力してはいけません。境界は 1rem、2rem の順を保ち、直線の方向は負の vw 係数で表します。
CSS 仕様では、矛盾する境界が指定された場合に最小値側の計算が優先されます。境界を並べ替えておけば、この挙動による誤りを避けられます。
固定値も正しい結果
両端がどちらも 16px なら、傾きはゼロです。長い形で書くことも技術的には可能です。
font-size: clamp(1rem, 1rem + 0vw, 1rem);
しかし、追加情報は何もありません。最小で正しい結果は次のとおりです。
font-size: 1rem;
すべての入力ペアにレスポンシブな式が必要であるかのように扱わず、この場合は簡略化すべきです。デザインシステムでも、補間が実際にレイアウトへ役立つ場合だけ流体トークンを使います。
流体文字は自動的にアクセシブルにはならない
流体タイポグラフィが解決するのは視覚的な補間です。それだけでアクセシビリティ要件を満たすわけではありません。
WCAG 2.2 達成基準 1.4.4では、明記された例外を除き、コンテンツや機能を失わずに文字を 200% まで拡大できることを求めています。MDN の clamp() に関するアクセシビリティ指針は、相対単位の最大値を最小値の少なくとも二倍にすることを、文字の拡大を早く止めすぎないための一つの方法として紹介しています。
この比率は有用な警告ですが、適合証明ではありません。最終的には実際の動作を確認します。
- ページを 200% にズームします。
- 文字を読めること、コントロールが消えないことを確認します。
- ラベルが切れずに折り返すことを確認します。
- 固定高のコンテナが内容を隠していないか確認します。
- 文字を拡大した状態で狭いレイアウトを再テストします。
W3C の G179 技法も同じ結果を重視しています。文字を拡大しても、コンテンツと機能が利用できなければなりません。
font-size の範囲が二対一未満の場合、ZeroTool 計算機は情報メッセージを表示します。ただし CSS の生成は止めません。製品ごとに制約は異なり、適切かどうかは実コンポーネントでのテストでしか判断できないためです。
よくある間違いと原因
1. 中央の値を平均値だと考える
推奨値は式であり、最小値と最大値の中間ではありません。clamp(1rem, 1.5rem, 2rem) のように中央も固定値なら、ビューポート幅に反応せず、常に 1.5rem になります。
2. 端点を確認せず vw だけを使う
2vw は一つのスクリーンショットではよく見えても、それ以外の幅でデザインから外れる可能性があります。公開前に切片を計算するか、少なくとも両端点を検算してください。
3. 傾きを早い段階で丸める
表示する CSS は小数点以下四桁でも構いませんが、計算は出力直前まで完全な精度を保ちます。早い丸めは切片を動かし、端点の検算を失敗させます。
4. + または - の周囲の空白を省く
CSS の数学構文では、二項の加算・減算記号の両側に空白が必要です。次のように書きます。
clamp(1rem, 0.7143rem + 1.4286vw, 2rem)
次の形は避けてください。
clamp(1rem, 0.7143rem+1.4286vw, 2rem)
5. ビューポート区間をゼロにする
最小・最大ビューポート幅が同じだと、傾きの計算でゼロ除算が発生します。固定値が必要だったのか、それとも異なる二つの端点を入力すべきかを見直します。
6. すべてのルートフォントを 16px だと仮定する
16px は一般的な既定値ですが、すべてのプロジェクトの契約ではありません。px の端点を rem に変換する前に、ルートスタイルとユーザー向けのフォント設定を確認してください。
7. コンテナ基準のコンポーネントでビューポート補間を使う
広いデスクトップ画面でも、その中のカードは狭いことがあります。vw はカードではなくページ全体に反応します。独立して再利用するコンポーネントでは、コンテナクエリとコンテナ相対単位を手書きする方が適切か検討してください。この計算機が生成するのは、意図的にビューポート基準の CSS です。
8. すべてのトークンを流体化する
境界線、アイコンのストローク、ベースライングリッドなど、正確さが必要な要素には安定した値が向いています。流体化は設計上の判断であり、既定のリファクタリングではありません。
小さな文字・スペーシングシステムを作る
端点が承認されたら、式を各コンポーネントに散らさず、カスタムプロパティとして保存します。
:root {
--font-body: clamp(1rem, 0.9643rem + 0.1786vw, 1.125rem);
--font-title: clamp(2rem, 1.5714rem + 2.1429vw, 3.5rem);
--space-section: clamp(3rem, 2.1429rem + 4.2857vw, 6rem);
--space-card: clamp(1rem, 0.8571rem + 0.7143vw, 1.5rem);
}
body {
font-size: var(--font-body);
}
h1 {
font-size: var(--font-title);
}
.section {
padding-block: var(--space-section);
}
トークンは役割で命名し、端点の前提をデザインシステムの文書に残し、生成値を一か所で管理します。式が決まったら、CSS 変数ジェネレーターで最終的な :root ブロックを組み立てられます。
一組の端点だけを使い、デザインレビューなしでモジュラーなタイプスケール全体を生成するのは避けてください。線形補間が答えるのは「この一つのトークンが二点間でどう変わるか」です。すべての見出しレベルが同じ比率や端点を共有すべきかどうかは決めません。
JavaScript で clamp 値を生成する
一度だけの作業にはブラウザーベースの計算機が最短です。ビルドツールやトークンパイプラインなら、同じ計算を依存なしの小さな関数にできます。
function clampFromPoints({
minViewport,
maxViewport,
minValue,
maxValue,
unit = 'rem',
root = 16,
}) {
if (!(maxViewport > minViewport)) {
throw new Error('maxViewport must be greater than minViewport');
}
if (unit === 'rem' && !(root > 0)) {
throw new Error('root must be greater than zero');
}
const round = value => Number(value.toFixed(4));
const length = px =>
unit === 'rem' ? `${round(px / root)}rem` : `${round(px)}px`;
if (Math.abs(maxValue - minValue) < 1e-9) {
return length(minValue);
}
const slope = (maxValue - minValue) / (maxViewport - minViewport);
const interceptPx = minValue - slope * minViewport;
const intercept = unit === 'rem' ? interceptPx / root : interceptPx;
const vw = slope * 100;
const sign = vw < 0 ? '-' : '+';
const lower = Math.min(minValue, maxValue);
const upper = Math.max(minValue, maxValue);
return `clamp(${length(lower)}, ${round(intercept)}${unit} ${sign} ${round(Math.abs(vw))}vw, ${length(upper)})`;
}
console.log(clampFromPoints({
minViewport: 320,
maxViewport: 1440,
minValue: 16,
maxValue: 32,
}));
// clamp(1rem, 0.7143rem + 1.4286vw, 2rem)
計算には丸めていない値を使い、CSS を文字列化するときだけ丸めます。トークンパイプラインが小数を文字列で受け取るなら、二つの端点、逆方向、固定値のテストを追加してください。
Python で同じ値を生成する
Python 版は、デザイントークンのスクリプトや静的サイトのビルド処理に向いています。
def clamp_from_points(v_min, v_max, s_min, s_max, root=16):
if v_max <= v_min:
raise ValueError("v_max must be greater than v_min")
if root <= 0:
raise ValueError("root must be greater than zero")
if abs(s_max - s_min) < 1e-9:
return f"{s_min / root:.4f}".rstrip("0").rstrip(".") + "rem"
slope = (s_max - s_min) / (v_max - v_min)
intercept_rem = (s_min - slope * v_min) / root
vw = slope * 100
lower, upper = sorted((s_min, s_max))
sign = "-" if vw < 0 else "+"
return (
f"clamp({lower / root:.4f}rem, "
f"{intercept_rem:.4f}rem {sign} {abs(vw):.4f}vw, "
f"{upper / root:.4f}rem)"
)
print(clamp_from_points(320, 1440, 16, 32))
実運用では、末尾の不要なゼロをプロジェクト全体で同じ形式に整えてください。重要なのは計算であり、フォーマット方針はほかの生成 CSS とそろえます。
Bash/awk で CI チェックを作る
小さな CI チェックのためだけに言語ランタイムを追加したくないこともあります。次のシェル関数は awk で px 版を出力します。
clamp_px() {
awk -v v1="$1" -v v2="$2" -v s1="$3" -v s2="$4" 'BEGIN {
if (v2 <= v1) exit 2
if (s1 == s2) {
printf "%.4gpx\n", s1
exit
}
m = (s2 - s1) / (v2 - v1)
b = s1 - m * v1
lo = s1 < s2 ? s1 : s2
hi = s1 > s2 ? s1 : s2
printf "clamp(%.4gpx, %.4gpx %s %.4gvw, %.4gpx)\n", \
lo, b, m < 0 ? "-" : "+", (m < 0 ? -m : m) * 100, hi
}'
}
clamp_px 320 1440 16 32
これは完全なトークンコンパイラーではなく、意図的に検算用の補助に限定しています。複数単位、スキーマ検証、ファイル出力が必要になったら、テスト済みの JavaScript または Python スクリプトの方が保守しやすくなります。
マージ前に CSS をテストする
短いチェックリストで、式とレイアウトの両方の問題を見つけられます。
- **端点チェック:**最小・最大ビューポートで計算済みの値を確認します。
- **中点チェック:**中央の値が意図した補間と一致するか確認します。
- **範囲外チェック:**さらに狭い・広い幅で境界が維持されるか確認します。
- **ズームチェック:**実際のコンテンツでブラウザーを 200% に拡大します。
- **ローカライズチェック:**lorem ipsum ではなく、対応言語の長い実文を使います。
- **コンテナチェック:**文字が折り返し、カードが縦に伸びられることを確認します。
- **トークンチェック:**ルートフォントサイズの前提がプロジェクトと一致するか確認します。
- **構文チェック:**推奨式の
+と-の両側に空白を残します。
計算機のカーブと五行のサンプル表は数値チェックを担当します。レイアウトチェックはブラウザーの開発者ツールとコンポーネントテストで行ってください。
最小限で確実なルール
実際の二つの端点から一本の直線を導き、境界を正しい順序に保ち、その値を利用するコンポーネントでテストします。方法はこれだけです。
clamp() の価値は、すべての CSS 長を式に変えることではなく、不要なブレークポイントの段差をなくすことにあります。正確な流体値は、置き換えるメディアクエリより理解しやすいはずです。例外の説明に一段落必要なら、そのレイアウトには明示的なブレークポイントの方が向いている可能性があります。
参考資料
- MDN:
clamp()— 構文、ブラウザー対応、例、アクセシビリティの指針。 - CSS Values and Units Level 4 —
min()、max()、clamp()の正式な定義。 - W3C:達成基準 1.4.4「テキストのサイズ変更」を理解する — 200% 拡大要件とその目的。
ZeroTool の関連ツール
- CSS 単位変換ツール — px/rem 変換とビューポート単位を検算します。
- CSS 変数ジェネレーター — 完成した式をカスタムプロパティとして整理します。
- Cubic Bezier ジェネレーター — レスポンシブな長さではなく、アニメーションのイージングカーブを作ります。