Stepping and snapping in numeric inputs

Status: work-in-progress.

Note: Some of this math is baked into standard <input type='number'> and <input type='range'> elements.

Bounded inputs

Clamp the value `val` to the `[min, max]` interval.

function clampInterval(val, min, max) {
return Math.min(Math.max(val, min), max);

In a cyclic input, when the value goes beyond one of the edges of the interval it gets wrapped around to the other edge of the interval. On the web, you'd see such an input for the time of day (on the 12-hour clock or the 24-hour clock), or for other periodic functions, such as a color's hue or an object's rotation.

Clamp the value `val` to the `[0, n)` cyclic interval.

function modulo(val, n) {
return (val % n + n) % n;

Clamp the value `val` to the `[min, max)` cyclic interval.

function moduloInterval(val, min, max) {
return modulo(val - min, max - min) + min;

Clamp the value `val` to the `[min, max]` cyclic interval.

function moduloIntervalInclusive(val, min, max) {
return val === max ? val : moduloInterval(val, min, max);
<input type='number' min='0' max='10' data-cyclic>

Snapping to a step size

We may wish to constrain the numeric input so that the input's value is always a multiple of a step size.

step = 1 means the input only accepts integers. We only have to choose among the four rounding functions: Math.floor, Math.round, Math.ceil, and Math.trunc. To generalize for other step sizes:

function toStep(val, step) {
return Math.round(val / step) * step;

If we want to align the steps to the min value:

function toStepWithOffset(val, step, offset) {
return toStep(val - offset, step) + offset;

When using a non-integer step size, floating-point arithmetic may make the result of toStep() unfit for display in the numeric input:

toStep(0.5, 0.2);
// => 0.6000000000000001

To round the number to a particular number of decimal points:

function toPrecision(val, precision = 0) {
const p = 10 ** precision;
return Math.round(val * p) / p;
function snapToStep(val, step, precision) {
return toPrecision(toStep(val, step), precision);

snapToStep(0.5, 0.2, 1);
// => 0.6

The chosen precision corresponds to the number of decimal points in step.