DEV Community

Cover image for Build a spin input
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Build a spin input

When users need to adjust a numerical value up or down, we often provide a user interface element to make it easy for them. That's where the spin input comes in handy.

HTML has a built-in input element for creating a spin input by setting the type attribute to number. This input only allows digits, which prevents users from entering invalid characters. When you click or tap on the input, two arrow buttons appear on the right side to increase or decrease the value.

Give it a try below:

Unfortunately, we can't easily customize the buttons, such as replacing them with our own images. But don't worry, in this post, we'll learn how to build a spin input with JavaScript DOM.

HTML Markup

To create the spin input, we'll need to start with the HTML markup. The layout includes an input element and two buttons for increasing and decreasing the value.

<div class="spin" id="spin">
    <input type="text" name="input" class="spin__input" value="50">
    <button class="spin__btn spin__btn--minus"></button>
    <button class="spin__btn spin__btn--plus"></button>
</div>
Enter fullscreen mode Exit fullscreen mode

We've created a container div with the class spin. Inside the container, there are two buttons with the classes spin__btn spin__btn--minus and spin__btn spin__btn--plus. These buttons will decrease and increase the value, respectively. Additionally, there's an input element with a default value of 50.

Instead of using the number input type, we're using the usual text attribute.

Basic styles

When creating buttons that require an up or down arrow, we have a few options. We can use a minus or plus sign, an image, or - as we'll explore in this post - fake triangles created with pure CSS.

To create a CSS triangle to represent the up and down arrow in buttons, we can use the ::before or ::after pseudo-element and some creative CSS. Here's an example of how to create an upward triangle:

.spin__btn--plus::before {
    content: '';
    width: 0;
    height: 0;

    border-left: 0.5rem solid transparent;
    border-right: 0.5rem solid transparent;
    border-bottom: 0.5rem solid rgb(203 213 225);
}
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we're adding a triangle before the spin__btn--plus class for styling purposes. The pseudo-element doesn't actually contain any content because we set the content property to an empty string.

To make the triangle invisible by default, we set the width and height properties to zero. Then, we use borders with varying colors and thicknesses to create the triangular shape. By setting the left and right borders to transparent, only the bottom border is visible. Finally, we set the color of the triangle with the border-bottom-color property.

We can use similar code with different border property values to create a downward-pointing triangle for our other button. Check out how the minus button could look with this styling:

.spin__btn--minus::before {
    content: '';
    width: 0;
    height: 0;

    border-left: 0.5rem solid transparent;
    border-right: 0.5rem solid transparent;
    border-top: 0.5rem solid rgb(203 213 225);
}
Enter fullscreen mode Exit fullscreen mode

Positioning the buttons

To position our buttons perfectly within the container, we'll need to set the position property of the container to relative. This creates a positioning context for its child elements.

.spin {
    position: relative;
}
Enter fullscreen mode Exit fullscreen mode

With this setup, we can use absolute positioning to place our buttons within the container. We set their position properties to absolute and specify their top and bottom values.

.spin__btn {
    position: absolute;
    position: absolute;
    right: 0;
    height: 50%;
}
.spin__btn--minus {
    top: 0;
}
.spin__btn--plus {
    bottom: 0;
}
Enter fullscreen mode Exit fullscreen mode

In the code snippet, we target all .spin__btn elements and set their position property to absolute. Then, we target each individual button and set its top or bottom properties depending on its desired position.

With these styles, our input element will be perfectly centered vertically within the container with our two arrow buttons positioned at either end.

Increasing and decreasing value

Now, we need to make our buttons functional by adding event listeners that update the value of our input element. We can do this easily with the addEventListener method.

const container = document.getElementById('spin');
const inputEle = container.querySelector('.spin__input');

const minusBtn = container.querySelector('.spin__btn--minus');
const plusBtn = container.querySelector('.spin__btn--plus');

const increase = () => {
    const currentValue = inputEle.value;
    const newValue = (currentValue === '') ? 0 : parseInt(currentValue, 10) + 1;
    inputEle.value = newValue;
};

const decrease = () => {
    const currentValue = inputEle.value;
    const newValue = (currentValue === '') ? 0 : parseInt(currentValue, 10) - 1;
    inputEle.value = newValue;
};

minusBtn.addEventListener('click', decrease);
plusBtn.addEventListener('click', increase);
Enter fullscreen mode Exit fullscreen mode

First, we select the input element and both buttons using document.getElementById and document.querySelector. Then, we add event listeners to the minus and plus buttons. These listeners call the decrease and increase methods on the input element, respectively, which decrease or increase the value by 1. It's that simple!

Accepting digits only

Previously, we used the number type for input, which prevented users from entering invalid characters. However, we've changed our approach and now need to restrict input to digits only.

To achieve this, we can handle the keydown event, which fires when a user presses a key. We can then verify whether the pressed key is a number by comparing it with the ^[0-9]+$ pattern.

If the key is not a number, we prevent the default behavior by calling preventDefault(). It's worth noting that we still allow users to use the Backspace and Delete keys to remove digits as normal.

Here's how we handle the keydown event:

const handleKeyDown = (e) => {
    if (!/^[0-9]+$/.test(e.key) && e.key !== 'Backspace' && e.key !== 'Delete') {
        e.preventDefault();
    }
};

inputEle.addEventListener('keydown', handleKeyDown);
Enter fullscreen mode Exit fullscreen mode

Enhancing user experience with keyboard shortcuts

In addition to clicking buttons, we can further improve the user experience by allowing them to adjust the value using keyboard shortcuts. For example, pressing the up arrow key can increase the value, while pressing the down arrow key can decrease it.

To enable this feature, we need to update the keydown event handler to detect if the user presses the left or right arrow keys and adjust the value accordingly.

Here's an updated version of our code that includes keyboard shortcuts to enhance the user experience.

const handleKeyDown = (e) => {
    switch (e.key) {
        case 'ArrowDown':
            decrease();
            break;
        case 'ArrowUp':
            increase();
            break;
        default:
            if (!/^[0-9]+$/.test(e.key) && e.key !== 'Backspace' && e.key !== 'Delete') {
                e.preventDefault();
            }
            break;
    }
};
Enter fullscreen mode Exit fullscreen mode

Setting limits on input values

When working with input values, it's often necessary to set a minimum and maximum range for possible values. This can easily be done using the min and max attributes.

<input min="0" max="100">
Enter fullscreen mode Exit fullscreen mode

To ensure that the input value falls within the desired range, we can update our increase and decrease functions to include the clamp function. The clamp function is a utility function that ensures a value falls within a given range. It takes three arguments: min, max, and value. The function checks if value is less than min, and returns min if it is. If value is greater than max, the function returns max. If value is within the range of min and max, the function returns value.

const clamp = (min, max, value) => Math.min(Math.max(value, min), max);
Enter fullscreen mode Exit fullscreen mode

With this in mind, we can update our code to include the clamp function in our increase and decrease functions. This ensures that the input value falls within the specified range.

By setting limits on input values, we can ensure that our code is more robust and less prone to errors.

const min = parseInt(target.getAttribute('min'), 10);
const max = parseInt(target.getAttribute('max'), 10);

const increase = () => {
    const currentValue = inputEle.value;
    const newValue = (currentValue === '') ? 0 : parseInt(currentValue, 10) + 1;
    inputEle.value = clamp(min, max, newValue);
};

const decrease = () => {
    const currentValue = inputEle.value;
    const newValue = (currentValue === '') ? 0 : parseInt(currentValue, 10) - 1;
    inputEle.value = clamp(min, max, newValue);
};
Enter fullscreen mode Exit fullscreen mode

And that's all! With only a few lines of JavaScript and CSS, we've created a spin input that allows you to adjust a numerical value up or down. Give it a try and see for yourself by playing around with the demo below.

See also


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

Top comments (0)