Synthwave Dial
Quantity stepper as a synthwave-grid dial — chrome +/− buttons with a scanline window that morphs to magenta when the count hits max, cyan when it hits min.
Synthwave Dial the 20th of 22 designs in the 22 CSS Button Group Designs collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<div class="cbgp-syn" role="group" aria-label="Quantity"> <button type="button" data-cbgp-syn-step="-1" aria-label="Decrease quantity">−</button> <input type="number" min="1" max="10" value="1" aria-label="Quantity" data-cbgp-syn /> <button type="button" data-cbgp-syn-step="1" aria-label="Increase quantity">+</button> </div>
.cbgp-syn {
display: inline-flex; align-items: stretch;
background: linear-gradient(180deg, #0a0a14 0%, #050810 100%);
border: 1px solid #ff5af1;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 14px rgba(255,90,241,0.25);
}
.cbgp-syn button {
width: 42px; height: 42px;
border: 0; cursor: pointer;
background:
linear-gradient(180deg, rgba(255,255,255,0.08), transparent),
rgba(8,10,18,0.92);
color: #ff5af1;
font: 800 18px/1 ui-monospace, monospace;
text-shadow: 0 0 6px rgba(255,90,241,0.6);
transition: background 0.18s, color 0.18s;
}
.cbgp-syn button:hover {
background: rgba(255,90,241,0.12);
color: #ffaaf0;
}
.cbgp-syn button:focus-visible { outline: 2px solid #00ffe0; outline-offset: -2px; }
.cbgp-syn button:disabled { opacity: 0.3; cursor: not-allowed; }
.cbgp-syn button:disabled:hover { background: rgba(8,10,18,0.92); color: #ff5af1; }
.cbgp-syn input {
width: 64px;
border: 0; outline: none;
background:
repeating-linear-gradient(0deg,
transparent 0 4px,
rgba(0,255,224,0.06) 4px 5px),
#0a0a14;
color: #00ffe0;
font: 800 16px/1 ui-monospace, monospace;
text-align: center;
text-shadow: 0 0 6px rgba(0,255,224,0.5);
border-left: 1px solid rgba(255,90,241,0.3);
border-right: 1px solid rgba(255,90,241,0.3);
-moz-appearance: textfield;
}
.cbgp-syn input::-webkit-outer-spin-button,
.cbgp-syn input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cbgp-syn.is-min input { color: #00ffe0; box-shadow: inset 0 0 16px rgba(0,255,224,0.3); }
.cbgp-syn.is-max input { color: #ff5af1; box-shadow: inset 0 0 16px rgba(255,90,241,0.4); } /* Synthwave-stepper — clamp + classify min/max state for color shift. */
document.querySelectorAll('.cbgp-syn').forEach(function (group) {
var input = group.querySelector('[data-cbgp-syn]');
var minus = group.querySelector('[data-cbgp-syn-step="-1"]');
var plus = group.querySelector('[data-cbgp-syn-step="1"]');
if (!input) return;
function update() {
var min = Number(input.min) || -Infinity;
var max = Number(input.max) || Infinity;
var val = Number(input.value) || 0;
if (minus) minus.disabled = val <= min;
if (plus) plus.disabled = val >= max;
group.classList.toggle('is-min', val <= min);
group.classList.toggle('is-max', val >= max);
}
group.querySelectorAll('[data-cbgp-syn-step]').forEach(function (btn) {
btn.addEventListener('click', function () {
var dir = parseInt(btn.dataset.cbgpSynStep, 10) || 0;
var min = Number(input.min) || -Infinity;
var max = Number(input.max) || Infinity;
var val = (Number(input.value) || 0) + dir;
input.value = String(Math.max(min, Math.min(max, val)));
update();
});
});
input.addEventListener('input', update);
update();
});