Генерация CSS fluid-значений на базе clamp()

Один и тот же компонент сегодня открывают мобильном, завтра - на огромном мониторе. Если размеры задавать голыми пикселями и медиазапросами, получается каша из магических чисел: шрифты, отступы и кнопки растут по разным формулам, поддерживать это больно.

В фронтенд‑сообществе уже давно прижился подход с fluid‑логикой, когда размеры (шрифтов, отступом и т.д.) указываются не жесткими значениями а через CSS‑функцию clamp(), которая позволяет задать минимальный, максимальный размер и формулу роста между ними. Примерно так: clamp(1rem, 0.875rem + 1.5vw, 2rem). Такой подход позволяет избавиться от множества медиазапросов.

Для вычисления таких значение можно использовать онлайн калькуляторы, но это не всегда удобно и не централизовано. Поэтому я написал небольшой PHP‑класс, который генерирует готовые CSS‑переменные с такими fluid‑значениями на основе понятных параметров.

В WordPress есть похожая функция для вычисления fluid‑значений типографики на основе настроек темы: wp_get_computed_fluid_typography_value()

Класс

GitHub
<?php

/**
 * Generates ready-to-use CSS variables with fluid values via `clamp()`, so that sizes in `rem`
 * smoothly change between two viewport widths and do not exceed specified minimums and maximums.
 *
 * Usage example:
 *
 *     $formatter = new Fluid_CSSVar_Generator(
 *         375,   // minimum viewport width in pixels
 *         1280,  // maximum viewport width in pixels
 *         150,   // scaling factor in percent
 *         0      // minimum size in pixels
 *     );
 *     echo $formatter->generate( [12, 48], 4 );
 *     // or get single variable:
 *     echo $formatter->get_css_var( 24 );
 *
 * @ver 1.0.1
 */
class Fluid_CSSVar_Generator {

	public int    $root_size_px = 16;
	public string $var_pattern = '--fluid{scale}-{px}px';

	/**
	 * @param int   $vw_min_width  The minimum viewport width in pixels.
	 * @param int   $vw_max_width  The maximum viewport width in pixels.
	 * @param float $scale_percent The scaling factor between minimum and maximum size.
	 * @param int   $min_size_px   The minimum size in pixels. Less than this size will not be used.
	 */
	public function __construct(
		private readonly int $vw_min_width  = 375,
		private readonly int $vw_max_width  = 1280,
		private readonly int $scale_percent = 150,
		private readonly int $min_size_px   = 0,
	){
		( $this->scale_percent <= 100 ) && throw new InvalidArgumentException( '$scale_percent must be greater than 1.' );
		( $this->min_size_px < 0 ) && throw new InvalidArgumentException( '$min_size_px cannot be negative.' );
	}

	/**
	 * @param array $px_range  Minimum and maximum size in pixels at the maximum viewport width.
	 * @param int   $step      The step between sizes in pixels.
	 */
	public function generate( array $px_range, int $step = 1 ): string {
		$result = [];
		for ( $px = $px_range[0]; $px <= $px_range[1]; $px += $step ) {
			$result[] = $this->get_css_var( $px );
		}
		return implode( "\n", $result );
	}

	/**
	 * @param int $px The maximum size in pixels at the maximum viewport width.
	 */
	public function get_css_var( int $px ): string {
		$vw_min_rem = ( $this->vw_min_width / 100 ) / $this->root_size_px;
		$vw_max_rem = ( $this->vw_max_width / 100 ) / $this->root_size_px;
		$vw_range   = $vw_max_rem - $vw_min_rem;

		$scale_factor = $this->scale_percent / 100;
		$font_max_rem = $px / $this->root_size_px;
		$font_min_rem = max( $font_max_rem / $scale_factor, $this->min_size_px / $this->root_size_px );

		if ( $font_max_rem <= $font_min_rem ) {
			$min = self::format_value( $font_min_rem );

			return strtr(
				$this->var_pattern . ': {min}rem;',
				[
					'{scale}' => $this->scale_percent,
					'{px}'    => $px,
					'{min}'   => $min,
				]
			);
		}

		$font_range = $font_max_rem - $font_min_rem;
		$slope      = $font_range / $vw_range;
		$intercept  = $font_min_rem - ( $slope * $vw_min_rem );

		return strtr(
			$this->var_pattern . ': clamp({min}rem, {intercept}rem + {slope}vw, {max}rem);',
			[
				'{scale}'     => $this->scale_percent,
				'{px}'        => $px,
				'{min}'       => self::format_value( $font_min_rem ),
				'{intercept}' => self::format_value( $intercept ),
				'{slope}'     => self::format_value( $slope ),
				'{max}'       => self::format_value( $font_max_rem ),
			]
		);
	}

	private static function format_value( float $value ): string {
		return number_format( $value, 3, '.', '' );
	}

}

Использование

Параметры конструктора позволяют задать минимальную и максимальную ширину вьюпорта в пикселях,
коэффициент масштабирования (в процентах) и минимальный размер в пикселях (меньше которого значение не опустится).

Метод get_css_var() принимает целевой размер в пикселях (максимальный размер при максимальной ширине вьюпорта)
и возвращает строку с готовым определением CSS‑переменной с использованием функции clamp().

$gen = new Fluid_CSSVar_Generator(
	375,   // минимальная ширина вьюпорта в пикселях
	1280,  // максимальная ширина вьюпорта в пикселях
	150,   // коэффициент масштабирования в процентах
	0      // минимальный размер в пикселях
);
echo $gen->get_css_var( 24 );   // --fluid150-24px: clamp(1.000rem, 0.793rem + 0.884vw, 1.500rem);

$gen = new Fluid_CSSVar_Generator(
	375,   // минимальная ширина вьюпорта в пикселях
	1280,  // максимальная ширина вьюпорта в пикселях
	200,   // коэффициент масштабирования в процентах
	12     // минимальный размер в пикселях
);
echo $gen->get_css_var( 16 ); // --fluid200-16px: clamp(0.750rem, 0.646rem + 0.442vw, 1.000rem);

Пример с указанием параметров конструктора:

$gen = new Fluid_CSSVar_Generator( 375, 1440, 200 );
echo $gen->get_css_var( 32 ); // --fluid200-32px: clamp(1.000rem, 0.648rem + 1.502vw, 2.000rem);

Пример изменения названия CSS‑переменной:

$gen = new Fluid_CSSVar_Generator(
	375,   // минимальная ширина вьюпорта в пикселях
	1280,  // максимальная ширина вьюпорта в пикселях
	150,   // коэффициент масштабирования
);
$gen->var_pattern = '--my-fluid{scale}-{px}';
echo $gen->get_css_var( 20 ); // --my-fluid150-20: clamp(0.833rem, 0.661rem + 0.737vw, 1.250rem);

Пример генерации группы переменных:

$scale = 130;
$gen = new Fluid_CSSVar_Generator( 375, 1280, $scale, 12 );
$gen->var_pattern = '--fluid{scale}-min12-{px}px';
$gen = new Fluid_CSSVar_Generator( 375, 1280, 150, 2 );

echo "/** Scale $scale%, min 12px */\n";
echo $gen->generate( [10, 30], 2 );

Получим:

/** Scale 130%, min 12px */
--fluid150-10px: clamp(0.417rem, 0.330rem + 0.368vw, 0.625rem);
--fluid150-12px: clamp(0.500rem, 0.396rem + 0.442vw, 0.750rem);
--fluid150-14px: clamp(0.583rem, 0.462rem + 0.516vw, 0.875rem);
--fluid150-16px: clamp(0.667rem, 0.529rem + 0.589vw, 1.000rem);
--fluid150-18px: clamp(0.750rem, 0.595rem + 0.663vw, 1.125rem);
--fluid150-20px: clamp(0.833rem, 0.661rem + 0.737vw, 1.250rem);
--fluid150-22px: clamp(0.917rem, 0.727rem + 0.810vw, 1.375rem);
--fluid150-24px: clamp(1.000rem, 0.793rem + 0.884vw, 1.500rem);
--fluid150-26px: clamp(1.083rem, 0.859rem + 0.958vw, 1.625rem);
--fluid150-28px: clamp(1.167rem, 0.925rem + 1.031vw, 1.750rem);
--fluid150-30px: clamp(1.250rem, 0.991rem + 1.105vw, 1.875rem);