UI Components Easy
Number Input
Numeric input with increment/decrement stepper buttons, min/max/step support, and keyboard navigation.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 480px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.section {
margin-bottom: 1.75rem;
}
.section-label {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #475569;
margin-bottom: 0.625rem;
}
/* ── Number Input ── */
.num-input {
display: inline-flex;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
overflow: hidden;
background: rgba(255, 255, 255, 0.04);
transition: border-color 0.2s;
}
.num-input:focus-within {
border-color: rgba(99, 179, 237, 0.5);
box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.12);
}
.num-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.75rem;
background: rgba(255, 255, 255, 0.05);
border: none;
color: #94a3b8;
font-size: 1.1rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
user-select: none;
}
.num-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
color: #f2f6ff;
}
.num-btn:active:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
}
.num-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.num-btn--dec {
border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.num-btn--inc {
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.num-field {
width: 5rem;
height: 2.75rem;
background: transparent;
border: none;
color: #f2f6ff;
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
text-align: center;
outline: none;
padding: 0;
-moz-appearance: textfield;
}
.num-field::-webkit-outer-spin-button,
.num-field::-webkit-inner-spin-button {
-webkit-appearance: none;
}
/* ── Disabled state ── */
.num-input--disabled {
opacity: 0.45;
cursor: not-allowed;
}
.num-input--disabled .num-field {
cursor: not-allowed;
}document.querySelectorAll(".num-input:not(.num-input--disabled)").forEach(function (widget) {
var min = widget.dataset.min !== undefined ? parseFloat(widget.dataset.min) : -Infinity;
var max = widget.dataset.max !== undefined ? parseFloat(widget.dataset.max) : Infinity;
var step = widget.dataset.step !== undefined ? parseFloat(widget.dataset.step) : 1;
var field = widget.querySelector(".num-field");
var dec = widget.querySelector(".num-btn--dec");
var inc = widget.querySelector(".num-btn--inc");
function parse(v) {
var n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
function round(n) {
// round to step precision to avoid floating-point drift
var decimals = (step.toString().split(".")[1] || "").length;
return parseFloat(n.toFixed(decimals));
}
function clamp(n) {
return Math.min(Math.max(n, min), max);
}
function set(n) {
var clamped = clamp(round(n));
field.value = clamped;
dec.disabled = clamped <= min;
inc.disabled = clamped >= max;
}
dec.addEventListener("click", function () {
set(parse(field.value) - step);
});
inc.addEventListener("click", function () {
set(parse(field.value) + step);
});
field.addEventListener("change", function () {
set(parse(field.value));
});
field.addEventListener("keydown", function (e) {
if (e.key === "ArrowUp") {
e.preventDefault();
set(parse(field.value) + step);
}
if (e.key === "ArrowDown") {
e.preventDefault();
set(parse(field.value) - step);
}
});
// initialise boundary state
set(parse(field.value));
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Number Input</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Number Input</h1>
<p class="demo-sub">Stepper input with min/max/step support and keyboard navigation.</p>
<section class="section">
<p class="section-label">Default</p>
<div class="num-input" data-step="1">
<button class="num-btn num-btn--dec" aria-label="Decrement">−</button>
<input class="num-field" type="text" inputmode="numeric" value="0" aria-label="Number value" />
<button class="num-btn num-btn--inc" aria-label="Increment">+</button>
</div>
</section>
<section class="section">
<p class="section-label">With min (0) and max (10)</p>
<div class="num-input" data-min="0" data-max="10" data-step="1">
<button class="num-btn num-btn--dec" aria-label="Decrement" disabled>−</button>
<input class="num-field" type="text" inputmode="numeric" value="0" aria-label="Number value, 0 to 10" />
<button class="num-btn num-btn--inc" aria-label="Increment">+</button>
</div>
</section>
<section class="section">
<p class="section-label">Step 0.5, min −5, max 5</p>
<div class="num-input" data-min="-5" data-max="5" data-step="0.5">
<button class="num-btn num-btn--dec" aria-label="Decrement">−</button>
<input class="num-field" type="text" inputmode="decimal" value="0" aria-label="Number value, −5 to 5" />
<button class="num-btn num-btn--inc" aria-label="Increment">+</button>
</div>
</section>
<section class="section">
<p class="section-label">Disabled</p>
<div class="num-input num-input--disabled" data-step="1">
<button class="num-btn num-btn--dec" aria-label="Decrement" disabled>−</button>
<input class="num-field" type="text" inputmode="numeric" value="42" aria-label="Number value" disabled />
<button class="num-btn num-btn--inc" aria-label="Increment" disabled>+</button>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>Number Input
Accessible numeric stepper input with increment and decrement buttons, respecting min, max, and step constraints.
Variants
| Variant | Description |
|---|---|
| Default | Basic stepper with no constraints |
| Min / Max | Clamped range with buttons disabled at boundary |
| Disabled | Non-interactive, visually dimmed state |
Implementation
Native <input type="number"> is hidden; value is managed in JS and displayed in a custom <input type="text"> to avoid browser-native spinner UI. Arrow-key and direct typing are both supported with clamping.