7 min read

Understanding Mathematical Functions in CSS: Creating animations using the round() function

Table of Contents

Introduction

CSS functions mod(), rem(), and round(), which were previously supported only in the Safari browser, are now supported in Chrome as well.

inital

The mod() Function

The mod() function in CSS calculates the remainder of dividing the first parameter by the second parameter, similar to the remainder operator (%) in JavaScript.

Example usage:

/* Examples of the CSS mod() function */

/* Numbers without units */
line-height: mod(7, 2); /* Results in 1 */
line-height: mod(14, 5); /* Results in 4 */
line-height: mod(3.5, 2); /* Results in 1.5 */

/* Percentages or dimensions with units */
margin: mod(15%, 2%); /* Results in 1% */
margin: mod(18px, 4px); /* Results in 2px */
margin: mod(19rem, 5rem); /* Results in 4rem */
margin: mod(29vmin, 6vmin); /* Results in 5vmin */
margin: mod(1000px, 29rem); /* Results in 72px (assuming root font size is 16px) */

/* Positive and negative values */
rotate: mod(100deg, 30deg); /* Results in 10deg */
rotate: mod(135deg, -90deg); /* Results in -45deg */
rotate: mod(-70deg, 20deg); /* Results in 10deg */
rotate: mod(-70deg, -15deg); /* Results in -10deg */

/* Calculations */
scale: mod(10 * 2, 1.7); /* Results in 1.3 */
rotate: mod(10turn, 18turn / 3); /* Results in 4turn */
transition-duration: mod(20s / 2, 3000ms * 2); /* Results in 4s */

The rem() Function

The rem() function in CSS calculates the division remainder of the first parameter divided by the second parameter, similar to the remainder operator (%) in JavaScript.

Example usage:

/* Values without units */
line-height: rem(21, 2); /* Returns 1 */
line-height: rem(14, 5); /* Returns 4 */
line-height: rem(5.5, 2); /* Returns 1.5 */

/* Percentages or other dimensional units */
margin: rem(14%, 3%); /* Returns 2% */
margin: rem(18px, 5px); /* Returns 3px */
margin: rem(10rem, 6rem); /* Returns 4rem */
margin: rem(26vmin, 7vmin); /* Returns 5vmin */
margin: rem(1000px, 29rem); /* Returns 72px (assuming root font size is 16px) */

/* Positive and negative values */
rotate: rem(200deg, 30deg); /* Returns 20deg */
rotate: rem(140deg, -90deg); /* Returns 50deg */
rotate: rem(-90deg, 20deg); /* Returns -10deg */
rotate: rem(-55deg, -15deg); /* Returns -10deg */

/* Calculations */
scale: rem(10 * 2, 1.7); /* Returns 1.3 */
rotate: rem(10turn, 18turn / 3); /* Returns 4turn */
transition-duration: rem(20s / 2, 3000ms * 2); /* Returns 4s */

Difference Between rem and mod

The mod function generates a result that is either zero or has the same sign as the divisor.

The rem function generates a result that is either zero or has the same sign as the numerator.

These two mathematical functions are seldom used in everyday development.

However, there is one exception: the round() function. This function has a more complex syntax, but it is much more practical.


Why the round() Function is Essential and Practical

Many people believe that the round() function is just for rounding. In fact, it is designed with great flexibility, allowing you to implement various rounding methods through different parameter settings. For example, using the JS API:

  • Math.ceil()
  • Math.floor()
  • Math.round()
  • Math.trunc()

So how are these implemented? Let’s take a look at an example.

Emulating Math.ceil() Rounding Up in JavaScript

The Math.ceil() function rounds a number up. In CSS, it can be represented as follows:

canvas {
  border: round(up, 1.01px, 1px) solid;
}

The final rendered border width will be 2px.

Here, “up” represents rounding up, and the third parameter ensures the final value is divisible by this number. For example, the result of round(up, 2.01px, 2px) is 4px.

Emulating Math.floor() Rounding Down to the Nearest Whole Number

Math.floor() rounds down to the nearest whole number, so the border size in the following CSS code is set to 1px.

canvas {
  border: round(down, 1.99px, 1px) solid;
}

Emulating Math.round() Rounding to the Nearest Whole Number

Syntax:

round( nearest, <valueToRound> , <roundingInterval> )

Alternatively:

round( <valueToRound> , <roundingInterval> )

This is what we commonly call rounding.

Emulating Math.trunc() (Truncating the Decimal Part)

Syntax:

round( to-zero, <valueToRound> , <roundingInterval> )

This means rounding valueToRound to the closest integer multiple of roundingInterval, moving towards zero.

for example:

canvas {
  border: round(to-zero, 5.5px, 2px) solid;
}

The border width is 4px, because 5.5px rounds towards zero, and the nearest multiple of 2px is 4px.

The calculated value of round(to-zero, 5.5px, 3px) is 3px, as the largest multiple of 3px smaller than 5.5px is 3px.


Practical Applications of the round() Function in CSS

Here are two practical examples.

Ensure the responsive font-size is always an integer

In mobile development, the rem unit is often adjusted based on screen size, for example:

html {
  font-size: clamp(16px, calc(100% + 4 * (100vw - 375px) / 105), 20px);
}

At this point, the size corresponding to 1rem could become a decimal.

Decimal values can cause issues, such as when the size of some SVG icons is expressed in rem. Due to the decimal, tiny visual gaps can appear along the edges of the icons.

The same problem can occur with rounded corners or box-shadow edges, where small gaps may appear.

In such cases, you can use the round() function to round the size to the nearest integer, thus avoiding these issues.

Example usage:

<ul>
  <li>line 1</li>
  <li>line 2</li>
  <li>line 3</li>
</ul>
ul {
  font-size: round(1rem, 1px);
}

Here, the font size of the <ul> list will always be an integer.

Without using the round() function, the font size could be a decimal.

Refer to the GIF below for a dynamic demonstration (without the round() function, the font size is a decimal 18.2476px; with the round() function, it becomes the integer 18px):

inital1

Simulating the effect of the steps() function in an animation.

The last parameter of the round() function represents the smallest unit value for rounding. Imagine this: if you have an animation ranging from 0 to 100, and you set the rounding unit to 20, the final computed values after applying the round() function would only be 0, 20, 40, 60, 80, or 100. This behaves similarly to the steps() function in CSS animations.

For example, consider a static loading icon:

<img src="loading.png" class="spin" />

The actual rendered effect is as follows, it’s a static one: (Provide a description or an example of what the static effect looks like for better context)

inital1

The following CSS code can create a loading rotation effect for this icon:

.spin {
  transform: rotate(round(calc(var(--seed) * 3.6deg), 45deg));
  animation: seed 1s linear infinite;    
}
@property --seed {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}
@keyframes seed {
  from { --seed: 0; }    
  to { --seed: 100; }    
}

inital1

Isn’t it cleverly implemented!

Although the steps() function can achieve the same effect, it requires a higher learning and understanding cost compared to the round() function. Its only real advantage is compatibility.

However, this example also highlights the potential applications of the round() function.


Finally

Currently, most of the mathematical functions mentioned in the specification are already supported.

The first group of supported functions included min(), max(), and clamp().

Next, support for functions like sin() and cos() was introduced last year.

Other mathematical functions, such as sqrt() for square roots, pow() for exponents, exp() for the exponential function, and log() for logarithms, were supported at the end of last year, marking the third wave.

The value rounding functions discussed in this article belong to the fourth batch.

The final batch is expected to include functions like abs() for absolute value and sign() for determining positive, negative, or zero.