Generating CSS fluid-values based on clamp()

The same component today is opened on mobile, tomorrow – on a huge monitor. If sizes are set in raw pixels and media queries, you get a mess of magic numbers: fonts, paddings and buttons grow according to different formulas, and maintaining this is painful.

In the front-end community, the approach with fluid logic has long been adopted, when sizes (fonts, paddings, etc.) are not specified as hard values but through the CSS function clamp(), which allows you to set a minimum, maximum size and a growth formula between them. Something like: clamp(1rem, 0.875rem + 1.5vw, 2rem). This approach helps to get rid of many media queries.

For calculating such values you can use online calculators, but this is not always convenient and not centralized. Therefore I wrote a small PHP class that generates ready CSS variables with such fluid-values based on understandable parameters.

In WordPress there is a similar function for calculating fluid typography values based on theme settings: wp_get_computed_fluid_typography_value()

Class

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, '.', '' );
	}

}

Usage

The constructor parameters allow setting the minimum and maximum viewport width in pixels,
the scaling factor (in percent) and the minimum size in pixels (below which the value will not drop).

The method get_css_var() accepts the target size in pixels (the maximum size at the maximum viewport width)
and returns a string with a ready CSS variable definition using the clamp() function.

$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);

Example with constructor parameters specified:

$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);

Example of changing the CSS variable name:

$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);

Example of generating a group of variables:

$gen = new Fluid_CSSVar_Generator( 375, 1280, 150, 2 );
echo $gen->generate( [2, 10] );

/*
--fluid150-2px: 0.125rem;
--fluid150-3px: clamp(0.125rem, 0.099rem + 0.110vw, 0.188rem);
--fluid150-4px: clamp(0.167rem, 0.132rem + 0.147vw, 0.250rem);
--fluid150-5px: clamp(0.208rem, 0.165rem + 0.184vw, 0.313rem);
--fluid150-6px: clamp(0.250rem, 0.198rem + 0.221vw, 0.375rem);
--fluid150-7px: clamp(0.292rem, 0.231rem + 0.258vw, 0.438rem);
--fluid150-8px: clamp(0.333rem, 0.264rem + 0.295vw, 0.500rem);
--fluid150-9px: clamp(0.375rem, 0.297rem + 0.331vw, 0.563rem);
--fluid150-10px: clamp(0.417rem, 0.330rem + 0.368vw, 0.625rem);
*/