28 CSS Input Fields14 / 28
Stepper Number
Quantity input with custom −/+ controls. Native <input type="number"> underneath keeps keyboard arrows, validation, and screen-reader semantics intact.
Published
The code
<label class="if-14">
<span class="if-14-label">Quantity</span>
<span class="if-14-wrap">
<button type="button" class="if-14-btn" data-if-14="-1" aria-label="Decrease">−</button>
<input type="number" name="qty" min="0" max="99" value="1" aria-label="Quantity" />
<button type="button" class="if-14-btn" data-if-14="1" aria-label="Increase">+</button>
</span>
</label><label class="if-14">
<span class="if-14-label">Quantity</span>
<span class="if-14-wrap">
<button type="button" class="if-14-btn" data-if-14="-1" aria-label="Decrease">−</button>
<input type="number" name="qty" min="0" max="99" value="1" aria-label="Quantity" />
<button type="button" class="if-14-btn" data-if-14="1" aria-label="Increase">+</button>
</span>
</label>.if-14 {
display: grid;
gap: 6px;
width: 100%;
max-width: 220px;
font-size: 11px;
color: #b8b6d4;
}
.if-14-label {
font-weight: 600;
}
.if-14-wrap {
display: grid;
grid-template-columns: 36px 1fr 36px;
background: #1a1a22;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
.if-14-wrap:focus-within {
border-color: #14b8a6;
}
.if-14-btn {
background: rgba(20, 184, 166, 0.06);
border: 0;
color: #5eead4;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.if-14-btn:hover {
background: rgba(20, 184, 166, 0.18);
color: #fff;
}
.if-14-btn:active {
background: rgba(20, 184, 166, 0.28);
}
.if-14 input {
background: transparent;
border: 0;
outline: none;
color: #f0eeff;
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 10px 0;
-moz-appearance: textfield;
}
.if-14 input::-webkit-outer-spin-button,
.if-14 input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}.if-14 {
display: grid;
gap: 6px;
width: 100%;
max-width: 220px;
font-size: 11px;
color: #b8b6d4;
}
.if-14-label {
font-weight: 600;
}
.if-14-wrap {
display: grid;
grid-template-columns: 36px 1fr 36px;
background: #1a1a22;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
.if-14-wrap:focus-within {
border-color: #14b8a6;
}
.if-14-btn {
background: rgba(20, 184, 166, 0.06);
border: 0;
color: #5eead4;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.if-14-btn:hover {
background: rgba(20, 184, 166, 0.18);
color: #fff;
}
.if-14-btn:active {
background: rgba(20, 184, 166, 0.28);
}
.if-14 input {
background: transparent;
border: 0;
outline: none;
color: #f0eeff;
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 10px 0;
-moz-appearance: textfield;
}
.if-14 input::-webkit-outer-spin-button,
.if-14 input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}// Stepper +/- buttons — clamp to min/max
document.querySelectorAll(".if-14-wrap").forEach(function (wrap) {
var input = wrap.querySelector('input[type="number"]');
if (!input) return;
wrap.querySelectorAll("[data-if-14]").forEach(function (btn) {
btn.addEventListener("click", function () {
var dir = parseInt(btn.dataset.ifStep, 10) || 0;
var min = input.min !== "" ? Number(input.min) : -Infinity;
var max = input.max !== "" ? Number(input.max) : Infinity;
var val = (Number(input.value) || 0) + dir;
if (val < min) val = min;
if (val > max) val = max;
input.value = String(val);
});
});
});// Stepper +/- buttons — clamp to min/max
document.querySelectorAll(".if-14-wrap").forEach(function (wrap) {
var input = wrap.querySelector('input[type="number"]');
if (!input) return;
wrap.querySelectorAll("[data-if-14]").forEach(function (btn) {
btn.addEventListener("click", function () {
var dir = parseInt(btn.dataset.ifStep, 10) || 0;
var min = input.min !== "" ? Number(input.min) : -Infinity;
var max = input.max !== "" ? Number(input.max) : Infinity;
var val = (Number(input.value) || 0) + dir;
if (val < min) val = min;
if (val > max) val = max;
input.value = String(val);
});
});
});Here's a working CSS Input Field from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Stepper Number
Source: https://codefronts.com/components/css-input-fields/stepper-number/
Quantity input with custom −/+ controls. Native <input type="number"> underneath keeps keyboard arrows, validation, and screen-reader semantics intact.
## HTML
```html
<label class="if-14">
<span class="if-14-label">Quantity</span>
<span class="if-14-wrap">
<button type="button" class="if-14-btn" data-if-14="-1" aria-label="Decrease">−</button>
<input type="number" name="qty" min="0" max="99" value="1" aria-label="Quantity" />
<button type="button" class="if-14-btn" data-if-14="1" aria-label="Increase">+</button>
</span>
</label>
```
## CSS
```css
.if-14 {
display: grid;
gap: 6px;
width: 100%;
max-width: 220px;
font-size: 11px;
color: #b8b6d4;
}
.if-14-label {
font-weight: 600;
}
.if-14-wrap {
display: grid;
grid-template-columns: 36px 1fr 36px;
background: #1a1a22;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
.if-14-wrap:focus-within {
border-color: #14b8a6;
}
.if-14-btn {
background: rgba(20, 184, 166, 0.06);
border: 0;
color: #5eead4;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.if-14-btn:hover {
background: rgba(20, 184, 166, 0.18);
color: #fff;
}
.if-14-btn:active {
background: rgba(20, 184, 166, 0.28);
}
.if-14 input {
background: transparent;
border: 0;
outline: none;
color: #f0eeff;
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 10px 0;
-moz-appearance: textfield;
}
.if-14 input::-webkit-outer-spin-button,
.if-14 input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
```
## JavaScript
```js
// Stepper +/- buttons — clamp to min/max
document.querySelectorAll(".if-14-wrap").forEach(function (wrap) {
var input = wrap.querySelector('input[type="number"]');
if (!input) return;
wrap.querySelectorAll("[data-if-14]").forEach(function (btn) {
btn.addEventListener("click", function () {
var dir = parseInt(btn.dataset.ifStep, 10) || 0;
var min = input.min !== "" ? Number(input.min) : -Infinity;
var max = input.max !== "" ? Number(input.max) : Infinity;
var val = (Number(input.value) || 0) + dir;
if (val < min) val = min;
if (val > max) val = max;
input.value = String(val);
});
});
});
```Here's a working CSS Input Field from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Stepper Number
Source: https://codefronts.com/components/css-input-fields/stepper-number/
Quantity input with custom −/+ controls. Native <input type="number"> underneath keeps keyboard arrows, validation, and screen-reader semantics intact.
## HTML
```html
<label class="if-14">
<span class="if-14-label">Quantity</span>
<span class="if-14-wrap">
<button type="button" class="if-14-btn" data-if-14="-1" aria-label="Decrease">−</button>
<input type="number" name="qty" min="0" max="99" value="1" aria-label="Quantity" />
<button type="button" class="if-14-btn" data-if-14="1" aria-label="Increase">+</button>
</span>
</label>
```
## CSS
```css
.if-14 {
display: grid;
gap: 6px;
width: 100%;
max-width: 220px;
font-size: 11px;
color: #b8b6d4;
}
.if-14-label {
font-weight: 600;
}
.if-14-wrap {
display: grid;
grid-template-columns: 36px 1fr 36px;
background: #1a1a22;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
.if-14-wrap:focus-within {
border-color: #14b8a6;
}
.if-14-btn {
background: rgba(20, 184, 166, 0.06);
border: 0;
color: #5eead4;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.if-14-btn:hover {
background: rgba(20, 184, 166, 0.18);
color: #fff;
}
.if-14-btn:active {
background: rgba(20, 184, 166, 0.28);
}
.if-14 input {
background: transparent;
border: 0;
outline: none;
color: #f0eeff;
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 10px 0;
-moz-appearance: textfield;
}
.if-14 input::-webkit-outer-spin-button,
.if-14 input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
```
## JavaScript
```js
// Stepper +/- buttons — clamp to min/max
document.querySelectorAll(".if-14-wrap").forEach(function (wrap) {
var input = wrap.querySelector('input[type="number"]');
if (!input) return;
wrap.querySelectorAll("[data-if-14]").forEach(function (btn) {
btn.addEventListener("click", function () {
var dir = parseInt(btn.dataset.ifStep, 10) || 0;
var min = input.min !== "" ? Number(input.min) : -Infinity;
var max = input.max !== "" ? Number(input.max) : Infinity;
var val = (Number(input.value) || 0) + dir;
if (val < min) val = min;
if (val > max) val = max;
input.value = String(val);
});
});
});
```How this works
A native type="number" input sits between two type="button" controls, so keyboard arrows, form submission, and screen-reader semantics stay intact. The data-if-14 attribute on each button carries the direction (-1 or +1); a delegated click handler reads the current input.value, adds the direction, and clamps against input.min/input.max before writing back.
Because the underlying element is a real number input, browser validation (:invalid, ValidityState), the mobile numeric keypad, and paste behaviour come for free. The label wraps everything so the whole control gets one accessible name — the pattern most e-commerce cart quantity steppers use.
Make it yours
- Swap
min/max/stepon the input to change bounds and increments — e.g.step="0.5"for half units. - Add hold-to-repeat with
mousedown+setInterval, clearing onmouseup/mouseleave/blur. - Announce the new count to assistive tech: mirror
input.valueinto anaria-live="polite"region on update. - Recolour
--accentor the.if-14-btnhover state to match your form's focus ring.
Gotchas — read before shipping
type="number"silently drops locale-formatted paste like"1,234"— parse with a regex before assigning if that matters.- If you add hold-to-repeat, clear the interval on
pointerupANDpointercancel, or a mouseup outside the window leaks the timer. - Firefox scroll-wheel on a focused number input changes the value — call
input.blur()on wheel or setreadonlyif unwanted.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 90+ | 14+ | 90+ | 90+ |
Uses only type="number", dataset, and grid — universal support since 2021.