面倒な計算はもう不要!CSSのclamp関数の活用テクニック

CSSのclamp関数を使用する際に、推奨値の計算をしたり、代わりにジェネレータを使用するのは結構面倒だな〜、と思ってclampを簡単に出力できるscss関数を作ってみました。
ジェネレータはググると色々出てきますが、remの値しか出力してくれないものが多く、pxで出せるものを探すのも結構面倒です。
clamp関数とは何かについては、以下のリンクで詳しく解説されています。
CSSの比較関数が便利すぎる! min(), max(), clamp()の使い方を詳しく解説 | コリス (coliss.com)

2025/01/01追記:
pxとremが混ざっていても処理できるように改善しました。メディアクエリはpxで指定していて、フォントサイズなどはremを使う場合にも対応できるようになりました。
コメントも充実させました。

2023/7/2追記:
使い勝手の改善と最大値と最小値がマイナスの場合などでも意図した通りの結果が出力されるように改善しました。

  • 使い勝手の改善:
    • 最小値と最大値が適用される画面幅を固定ではなく、オプション引数で指定可能に変更
  • 特殊な状況での出力結果の修正:
    • グラフの傾斜が逆になる(右に向かって下がる)場合に発生していた不具合を解消

コード

指定した最小値と最大値に応じて、適用される画面幅に合わせた値を返します。

/**
 * clamp関数の文字列を返す
 *
 * @param {Number} $size-at-min-width - 最小画面幅での要素のサイズ (px|rem|emなど)
 * @param {Number} $size-at-max-width - 最大画面幅での要素のサイズ (px|rem|emなど)
 * @param {Number} $min-width [optional] - 最小画面幅 (デフォルト: $min-width-default)
 * @param {Number} $max-width [optional] - 最大画面幅 (デフォルト: $max-width-default)
 * @return {String} CSS clamp関数を含む計算式
 *
 * @description
 * 画面幅に応じて値が滑らかに変化するレスポンシブな値を生成します。
 * 例えば、フォントサイズやマージン、パディングなどの値を画面幅に応じて
 * 自動的に調整することができます。
 *
 * @example
 *   // フォントサイズを16pxから24pxまで可変させる
 *   font-size: clamp-calc(16px, 24px);
 *
 *   // マージンを2remから4remまで可変させる(画面幅768px~1200px)
 *   margin: clamp-calc(2rem, 4rem, 768px, 1200px);
 *
 * @note
 * - 引数の単位は一貫している必要はありません(px, rem等が混在可能)
 * - 内部で全ての値をpxに変換して計算を行います
 * - 返り値は入力された$size-at-min-widthと同じ単位で返されます
 * - 負の値(マイナスマージンなど)にも対応しています
 *
 * @implementation
 * 1. 入力値を全てpxに変換
 * 2. 線形の傾きを計算
 * 3. y軸との交点を計算
 * 4. 必要に応じて最小値と最大値を入れ替え
 * 5. 元の単位に変換して最終的なclamp関数を構築
 */
$base-font-size: $rem-to-px-size !default; // 基準となるフォントサイズ
$min-width-default: $bp-sm; // デフォルトの最小画面幅
$max-width-default: $bp-lg; // デフォルトの最大画面幅
@function clamp-calc(
  $size-at-min-width,
  $size-at-max-width,
  $min-width: $min-width-default,
  $max-width: $max-width-default
) {
  // 基準となる単位を格納 (返り値は、この基準単位で返す)
  $base-unit: unit($size-at-min-width);
  // 全値をpxに変換して計算の統一性を確保
  $size-at-min-width: convert-to-px($size-at-min-width);
  $size-at-max-width: convert-to-px($size-at-max-width);
  $min-width: convert-to-px($min-width);
  $max-width: convert-to-px($max-width);

  // レスポンシブな変化の傾きを計算
  // (最大サイズ - 最小サイズ) / (最大幅 - 最小幅)
  $slope: calc(($size-at-max-width - $size-at-min-width) / ($max-width - $min-width));

  // y軸との交点を計算(線形方程式のy切片)
  // y = mx + b の b を求める
  $y-axis-intersection: -1 * $min-width * $slope + $size-at-min-width;
  // 小数点以下3桁で四捨五入
  $y-axis-intersection: round-decimal($y-axis-intersection, 3);
  // $slope(傾き)が単位なしの数値なので、単位をvwにする
  $slope-vw: calc($slope * 100vw);
  // 小数点以下3桁で四捨五入(必須ではないが、ブラウザの検証ツールで見た時に数値が短く済むように使用)
  $slope-vw: round-decimal($slope-vw, 3);

  // 傾斜が逆方向になる場合、clamp関数の引数の最小値と最大値を入れ替える
  // ※例えば、マイナスマージンで画面幅が広い時に絶対値での数値が大きい場合に発生する。
  //  入れ替えないと、画面幅に関係なくサイズの最小値が適用されてしまう。
  @if $size-at-max-width < $size-at-min-width {
    $temp-max: $size-at-max-width;
    $temp-min: $size-at-min-width;
    $size-at-max-width: $temp-min;
    $size-at-min-width: $temp-max;
  }
  // 基準単位がremの場合は、結果をremに変換
  @if $base-unit == 'rem' {
    $size-at-min-width: convert-to-rem($size-at-min-width);
    $size-at-max-width: convert-to-rem($size-at-max-width);
    $y-axis-intersection: convert-to-rem($y-axis-intersection);
  }
  // 最終的なclamp関数を構築して返す
  @return clamp(#{$size-at-min-width}, #{$y-axis-intersection} + #{$slope-vw}, #{$size-at-max-width});
}

/**
 * 与えられた値をピクセル(px)単位に変換する関数
 *
 * @param {Number} $value - 変換したい値(rem または px)
 * @return {Number} 変換後のピクセル値
 *
 * @example
 *   convert-to-px(1.5rem)  // 24px ($base-font-size が 16px の場合)
 *   convert-to-px(20px)    // 20px (そのまま返される)
 *   convert-to-px(2em)     // 2em (非対応の単位はそのまま返される)
 *
 * @description
 * - rem単位の場合: $base-font-sizeを基準にしてpxに変換
 * - px単位の場合: 値をそのまま返す
 * - その他の単位: 変換せずそのまま返す
 *
 * @throws {Error} $base-font-size が定義されていない場合にエラー
 */
@function convert-to-px($value) {
  $unit: unit($value);

  @if $unit == 'rem' {
    @return calc($value / 1rem * $base-font-size);
  } @else if $unit == 'px' {
    @return $value;
  }
  @return $value; // その他の単位の場合はそのまま返す
}

/**
 * ピクセル(px)単位の値をrem単位に変換する関数
 *
 * @param {Number} $px-value - 変換したい値(px または rem)
 * @return {Number} 変換後のrem値
 *
 * @example
 *   convert-to-rem(16px)   // 1rem ($base-font-size が 16px の場合)
 *   convert-to-rem(24px)   // 1.5rem ($base-font-size が 16px の場合)
 *   convert-to-rem(1.5rem) // 1.5rem (そのまま返される)
 *   convert-to-rem(2em)    // 2em (非対応の単位はそのまま返される)
 *
 * @description
 * - px単位の場合: $base-font-sizeを基準にしてremに変換
 * - rem単位の場合: 値をそのまま返す
 * - その他の単位: 変換せずそのまま返す
 *
 * @note
 * - レスポンシブデザインに適したrem単位への変換に使用
 * - $base-font-size はグローバルで定義されている必要がある
 *
 * @throws {Error} $base-font-size が定義されていない場合にエラー
 */
@function convert-to-rem($px-value) {
  @if unit($px-value) == 'px' {
    $number: calc($px-value / 1px);
    @return calc($number / ($base-font-size / 1px)) + rem;
  } @else if unit($px-value) == 'rem' {
    @return $px-value;
  }
  @return $px-value; // その他の単位の場合はそのまま返す
}

/*
 * 補助関数:小数点以下の指定した桁数で四捨五入する関数
 */
@function round-decimal($value, $decimal-place) {
  // 四捨五入する値($value)を10の小数点以下桁数累乗倍する
  $temp-value: calc($value * pow(10, $decimal-place));
  // 累乗倍した数値で四捨五入(roundは小数点以下の指定ができない)
  $temp-value: round($temp-value);
  // 四捨五入した値を再度同じ10の累乗倍の数値で割った数値を返す
  @return calc($temp-value / pow(10, $decimal-place));
}

/*
 * 補助関数:累乗を計算する関数
 * 引数:$number 底となる数
 *      $exponent 指数(正の整数のみ対応)
 */
@function pow($number, $exponent) {
  $value: 1;

  @if $exponent > 0 {
    @for $i from 1 through $exponent {
      $value: $value * $number;
    }
  }
  @return $value;
}

round-decimal()とpow()関数は無くても機能します。
scssの標準の関数では小数点以下の桁数を指定した四捨五入ができないのでround-decimal()を作りました。
そのround-decimal()の中で累乗する必要があるので、pow()も作りました。
※powはpower(累乗)の略です。
四捨五入しないと、小数点以下の数値が長くなって読みにくいものの、実際の画面表示上は四捨五入してもしなくても目視でわかる差が出ないので私は四捨五入しています。

// 四捨五入した場合
font-size: clamp(16px, 11.018px + 0.877vw, 20px);
// 四捨五入しない場合
font-size: clamp(16px, 11.0175438596px + 0.8771929825vw, 20px);

使い方

事前に関数の外で、最小値と最大値が適用される画面幅のデフォルト値をそれぞれ定義しておきます。以下は、上のコードの抜粋です。

$min-width-default: 568px; // デフォルトの最小画面幅
$max-width-default: 1024px; // デフォルトの最大画面幅

あとは、scssのコードの中で以下のように引数に最小値と最大値を指定して使います。
font-sizeだけでなく、paddingやmarginなどでも使えます。(私はこれでメディアクエリを使う頻度が激減しました)

font-size: clamp-calc(16px, 20px);

最小値と最大値が適用される画面幅をデフォルト値以外にしたい場合は、引数にそれぞれの値を追加します。

// 第3引数が最小値が適用される画面幅(この例では375px)
// 第4引数が最大値が適用される画面幅(この例では1500px)
font-size: clamp-calc(16px, 20px, 375px, 1500px);

関数の説明(一部のみ)

clampを使うにあたり気になったのは、指定した画面幅以下になってfont-sizeが小さくなり始めてから最小値になるまでの変化が早いことです。
理想としては、2点のブレークポイントで綺麗に最大値と最小値になると気持ちいいです。
気にしなければどうでも良いのですが、細かいことが気になるタチなので、今後clampを使うたびにモヤモヤするのもなーと思いまして。
clamp-calc()の中に出てくる変数でわかりにくい $slope と $y-axis-intersection をグラフに表してみました。

$y-axis-intersection は、昔学校で習った [y = 0.5x + 10] のような方程式の[10]の部分ですね。
ちなみに、今回の関数でこのような方程式の[x]に当たるのが画面幅(vw)で、[0.5]は $slope です。

最大値と最小値が逆転してして、グラフの傾斜($slope)が右に向かって下がる場合に、clamp関数の引数の最大値と最小値を入れ替える必要があります。
$slopeなどの計算では、最大値と最小値の位置付けは変わりませんが、clamp関数に渡す引数では、最大値と最小値を入れ替えないと、ずっと最小値が適用されてしまうという不具合があったので以下のコードを追加しました。
※例えば、マイナスマージンで最大値と最小値が両方マイナスの場合に、絶対値では最大値の方が大きくても、相対値では最大値の方が小さい状態になります。
もう少し具体的な例だと、margin-topで画面幅が広い時には -48px、画面幅が狭い時には -16px にしたい場合などです。

  // グラフの傾斜が逆方向になる場合、clamp関数の引数の最小値と最大値を入れ替える
  // ※例えば、マイナスマージンで画面幅が広い時に絶対値での数値が大きい場合に発生する。
  //  入れ替えないと、画面幅に関係なくサイズの最小値が適用されてしまう。
  @if $size-at-max-width < $size-at-min-width {
    $temp-max: $size-at-max-width;
    $temp-min: $size-at-min-width;
    $size-at-max-width: $temp-min;
    $size-at-min-width: $temp-max;
  }

おわりに

最近になってやっとclampを使ったら、あまりの便利さに驚愕して、使ってこなかった自分を呪いました。
でも推奨値の計算はもとより、y方向の開始点を計算するのがかなり面倒だったのでマルっと関数にしたら、思った通りめちゃくちゃ楽になりました。
使えば使うほど、メディアクエリの使用頻度が激減して、clamp関数の偉大さを実感する日々です。

参考にしたサイト

感謝です。
Easy CSS Clamp SCSS Mixin – DEV Community
Power Function | CSS-Tricks – CSS-Tricks