Native CSS math for grid sizes and cyclical values
With round(), mod() and rem(), the browser computes values directly in the stylesheet declaration, without a script pre-calculating numbers and injecting them as a custom property. Anyone who needs to snap sizes to a grid or repeat values cyclically, for example carousel indices or stripe patterns, gets a tool that can be embedded directly inside calc().
Table of Contents
- 1. Why CSS now needs its own rounding and modulo functions
- 2. round(): snapping values to a step size
- 3. mod(): cyclical values for patterns and indices
- 4. rem() vs. mod(): the difference with negative numbers
- 5. Combining with calc(): nested expressions
- 6. Browser support and feature detection
- 7. Practical example: a CSS grid with guaranteed integer track widths
- 8. Practical example: carousel states with mod() and no JavaScript counter
- 9. Limits and common pitfalls
- 10. Summary
- 11. FAQ
1. Why CSS now needs its own rounding and modulo functions
Until recently, CSS had no way to round a computed value to a specific step size or express a cyclical repetition without falling back on JavaScript or a fixed set of hand-written selectors. A layout that needed to snap elements to an 8-pixel grid either had to pick every value by hand or let a script compute values on page load and inject them as a style attribute. That works, but breaks every time viewport width or another input value changes in a way CSS itself could react to.
With round(), mod() and rem() from CSS Values and Units Level 4, the language gets exactly this capability built in. All three are pure math functions that, like calc(), can appear anywhere a numeric value is expected, and they combine freely with calc(), custom properties and even container query units. That moves logic that used to require JavaScript back into the declaration, where it is easier to maintain and faster to render.
2. round(): snapping values to a step size
The function round(<strategy>, <value>, <interval>) rounds a value to a multiple of the given interval. The strategy determines the rounding direction: nearest rounds to the closest multiple, up always rounds upward, down always downward, and to-zero rounds toward zero. Omitting the strategy behaves like round(nearest, ...).
This becomes practical the moment a layout needs to align to a fixed base grid, for example an 8-pixel spacing system. Instead of manually snapping every width to a multiple of 8, round() handles it for computed, dynamic values, for example when an element's width depends on viewport width but still needs to land on the grid.
.grid-item {
/* Element width depends on the viewport but always lands
on a multiple of 8px */
width: round(nearest, 100vw / 3, 8px);
}
.card {
/* Padding never gets smaller than 8px, always rounded up
to 8px steps -- avoids odd in-between values */
padding: round(up, 1.2rem, 8px);
}
3. mod(): cyclical values for patterns and indices
mod(A, B) computes the remainder of dividing A by B, taking the sign of B. The result therefore always falls in the same sign range as the divisor, regardless of whether A is positive or negative. This matches the classic mathematical modulo operation known from Python, making it a perfect fit for cyclical patterns where negative intermediate values still need to wrap sensibly.
A typical use case is a carousel or gallery where an active index is set through a custom property and CSS needs to derive a cyclical state from it, for example highlighting every third tile regardless of how high the overall index counts. Stripe patterns using nth-child-like logic over computed values also benefit from mod() to express the wraparound without extra classes.
.carousel {
--active-index: 7;
/* Reduces an arbitrarily high index to the 0..2 range,
e.g. to always pick one of three state colors */
--cycle: mod(var(--active-index), 3);
}
.stripe {
/* Gradient offset wraps cyclically, negative values still
stay within the 0..359 range */
--hue-offset: mod(var(--index) * 47, 360deg);
background: hsl(var(--hue-offset) 70% 50%);
}
4. rem() vs. mod(): the difference with negative numbers
At first glance, mod() and rem(A, B) look identical, since both compute a division remainder. The crucial difference only shows up with negative input values: rem() takes the sign of A, the dividend, while mod() takes the sign of B, the divisor. With purely positive values both functions return exactly the same result, which is why the distinction gets glossed over in many tutorials.
For a layout working with negative offsets, for example a position that can shift left or right relative to an anchor point, rem() often gives the more intuitive result, because a negative input produces a negative result instead of jumping into the positive range. For cyclical states like carousel indices, on the other hand, mod() is almost always the right choice, since a clean, always-positive wraparound is what's wanted.
5. Combining with calc(): nested expressions
All three functions are full-fledged mathematical CSS expressions and can be nested inside calc() or used as an argument to another math function. This allows complex layout rules in a single declaration, for example a width that first derives a cyclical offset through mod() and then combines it with a fixed base size via calc().
It matters that the arguments stay type-compatible: mixing pixels with degrees without a shared unit or a deliberate conversion produces an invalid value that the browser silently ignores. It therefore pays off to consistently declare custom properties for intermediate values with an explicit unit, rather than passing unitless numbers through deeply nested calc() expressions.
.pattern-tile {
--i: 5;
/* mod() produces a cyclical offset 0..2, calc() adds
that to a fixed base width -- all in one declaration */
width: calc(120px + mod(var(--i), 3) * 20px);
}
.snapped {
/* nested: round first, then reuse the result inside
another calc() */
margin-top: calc(round(nearest, var(--raw-offset), 4px) + 2px);
}
6. Browser support and feature detection
round(), mod() and rem() have shipped in current versions of Chrome, Edge, Firefox and Safari since 2023/2024 and now count as production ready for projects that don't need to support very old browser versions. For projects with stricter support requirements, checking current numbers before adoption is worthwhile, since the minimum supported version differs per browser.
A reliable safeguard is @supports: inside an @supports (width: round(nearest, 1px, 1px)) rule, you can test whether the browser understands the function, and a fallback value outside the rule automatically applies for older browsers. That keeps the layout functional even without native rounding, just without the exact grid alignment.
.grid-item {
/* Fallback for browsers without round() support */
width: 33.33vw;
}
@supports (width: round(nearest, 1px, 1px)) {
.grid-item {
width: round(nearest, 100vw / 3, 8px);
}
}
7. Practical example: a CSS grid with guaranteed integer track widths
A common problem in responsive grids is sub-pixel widths that can cause visible misalignment between grid tracks, especially with image galleries that have visible borders. With round(), each track width can be explicitly forced to a whole pixel number instead of relying on the browser's sub-pixel rounding, which can vary slightly between rendering engines.
In practice this is often combined with clamp() to additionally set a minimum and maximum width, while round() handles grid alignment within those bounds. The result is a grid that looks clean on every viewport without JavaScript recalculating anything on resize.
8. Practical example: carousel states with mod() and no JavaScript counter
An image carousel with several visible tiles often needs to know a tile's position relative to the currently active tile, to scale or dim it accordingly. With mod(), this relative state can be computed directly from a running number and the current index position, without JavaScript having to tag every tile with a separate class.
That drastically reduces the amount of DOM manipulation: JavaScript only sets a single custom property for the active index, and CSS uses mod() to independently compute for every tile whether it is active, adjacent, or far away. That keeps interaction logic lean and style computation where it executes fastest.
.carousel-item {
--distance: mod(var(--item-index) - var(--active-index) + var(--total), var(--total));
}
.carousel-item[style*="--distance: 0"] {
transform: scale(1.1);
opacity: 1;
}
9. Limits and common pitfalls
A common trap is assuming round(), mod() and rem() can express arbitrarily complex logic at runtime like JavaScript functions do. In reality, these are pure, declarative math functions without conditionals, loops or state management, which is why more complex state machines still rely on custom properties and targeted selectors. For simple rounding and cycle logic, though, they are entirely sufficient and replace exactly the part that used to require JavaScript.
A second pitfall is division by zero: mod(A, 0) and rem(A, 0) produce an invalid value that the browser treats like a malformed declaration and ignores. Anyone pulling the divisor from a custom property that could theoretically be set to zero should provide a fallback through var(--divisor, 1) to guard against that scenario.
| Function | Computation | Sign of the result | Typical use |
|---|---|---|---|
round(nearest, A, B) |
Rounds A to a multiple of B | same as A | Grid sizes, spacing systems |
round(up, A, B) |
Always rounds A upward to B | same as A | Guaranteeing minimum spacing |
mod(A, B) |
Remainder of A / B | same as B, the divisor | Cyclical patterns, indices |
rem(A, B) |
Remainder of A / B | same as A, the dividend | Relative offsets with negative values |
Mironsoft
Modern CSS, layout architecture and rendering performance
CSS that stays maintainable instead of breaking with every change?
We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.
CSS Audit
Systematically uncovering specificity issues, cascade conflicts and unused selectors.
Architecture Refactoring
Introducing cascade layers, custom properties and design tokens cleanly.
Performance Tuning
Fixing layout thrashing, expensive selectors and rendering bottlenecks.
10. Summary
round(), mod() and rem() in CSS: The Essentials at a Glance
round()
Rounds a value to a multiple of an interval, using the nearest, up, down or to-zero strategies.
mod()
Division remainder that takes the sign of the divisor, ideal for cyclical states like carousel indices.
rem()
Division remainder that takes the sign of the dividend, better suited to negative relative offsets.
Combination
All three functions can be nested inside calc() freely, as long as argument units stay compatible.