Number Input
A numeric input with increment/decrement buttons. Built on <input type="number"> with custom stepper controls.
Default
Number input with +/- buttons.
<div class="number-input">
<button data-action="decrement">−</button>
<input type="number" value="1" min="0" max="99" step="1">
<button data-action="increment">+</button>
</div>
With step
<input type="number" value="20.0" min="-40" max="60" step="0.5">
CSS view file
/* -- Number Input component ------------------------------------- */
@layer components {
.number-input {
display: inline-flex;
align-items: center;
border: 1px solid var(--input);
border-radius: var(--radius-md);
background: var(--background);
box-shadow: var(--shadow-xs);
overflow: hidden;
& input {
border: none;
outline: none;
background: transparent;
height: 2.5rem;
width: 4rem;
text-align: center;
font-size: 0.875rem;
font-family: var(--font-sans);
color: var(--foreground);
-moz-appearance: textfield;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
& button {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2.5rem;
border: none;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
font-size: 1rem;
font-family: var(--font-sans);
transition: background 150ms, color 150ms;
flex-shrink: 0;
&:hover {
background: var(--accent);
color: var(--accent-foreground);
}
&:first-child {
border-right: 1px solid var(--input);
}
&:last-child {
border-left: 1px solid var(--input);
}
}
&:focus-within {
border-color: var(--ring);
box-shadow: 0 0 0 2px oklch(from var(--ring) l c h / 0.2);
}
}
}
JavaScript view file
Number Input
// -- Number Input ---------------------------------------------
// Increment/decrement buttons for .number-input containers.
function init() {
document.querySelectorAll('.number-input:not([data-init])').forEach((wrapper) => {
wrapper.dataset.init = '';
const input = wrapper.querySelector('input[type="number"]');
const decBtn = wrapper.querySelector('[data-action="decrement"]');
const incBtn = wrapper.querySelector('[data-action="increment"]');
if (!input) return;
const update = (direction) => {
try {
if (direction > 0) input.stepUp();
else input.stepDown();
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
} catch(e) { /* min/max boundary */ }
};
if (decBtn) decBtn.addEventListener('click', () => { update(-1); });
if (incBtn) incBtn.addEventListener('click', () => { update(1); });
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });