GitHub

Components / Form & Input / Stepper

Stepper beta

A number with minus and plus on either side, cast as one control. Bounds are enforced, not suggested.

Preview
<div style="display:flex;gap:26px;flex-wrap:wrap;align-items:center">
  <label style="display:grid;gap:6px;font-size:12.5px;font-weight:550">Seats
    <span class="k-stepper" data-stepper data-min="1" data-max="8">
      <button type="button" data-step-btn="-1" aria-label="Decrease seats">−</button>
      <output data-step-val aria-live="polite">2</output>
      <button type="button" data-step-btn="1" aria-label="Increase seats">+</button>
    </span>
  </label>
  <label style="display:grid;gap:6px;font-size:12.5px;font-weight:550">Copies
    <span class="k-stepper" data-stepper data-min="1" data-max="5">
      <button type="button" data-step-btn="-1" aria-label="Decrease copies">−</button>
      <output data-step-val aria-live="polite">5</output>
      <button type="button" data-step-btn="1" aria-label="Increase copies">+</button>
    </span>
  </label>
</div>
/* KATA Stepper */
.k-stepper{display:inline-flex;align-items:stretch;height:38px;border:1px solid var(--k-line);border-radius:var(--k-radius);background:var(--k-bg);overflow:hidden}
.k-stepper button{width:37px;border:0;background:transparent;color:var(--k-ink);font-family:var(--k-font);font-size:16px;cursor:pointer;transition:background .13s}
.k-stepper button:hover:not(:disabled){background:var(--k-bg-soft)}
.k-stepper button:disabled{opacity:.35;cursor:not-allowed}
.k-stepper button:focus-visible{outline:none;box-shadow:inset var(--k-ring)}
.k-stepper output{display:flex;align-items:center;justify-content:center;min-width:46px;padding:0 6px;border-left:1px solid var(--k-line);border-right:1px solid var(--k-line);font-family:var(--k-mono);font-size:13.5px;color:var(--k-ink)}
// KATA Stepper — bounded increment control.
document.querySelectorAll('[data-stepper]').forEach(function (root) {
  var out = root.querySelector('[data-step-val]');
  var min = parseFloat(root.dataset.min || '-Infinity');
  var max = parseFloat(root.dataset.max || 'Infinity');
  var step = parseFloat(root.dataset.step || '1');
  function render() {
    var v = parseFloat(out.textContent) || 0;
    root.querySelectorAll('[data-step-btn]').forEach(function (b) {
      var d = parseFloat(b.dataset.stepBtn);
      b.disabled = (d < 0 && v <= min) || (d > 0 && v >= max);
    });
  }
  root.querySelectorAll('[data-step-btn]').forEach(function (b) {
    b.addEventListener('click', function () {
      var v = parseFloat(out.textContent) || 0;
      v = Math.min(max, Math.max(min, v + parseFloat(b.dataset.stepBtn) * step));
      out.textContent = String(v);
      render();
    });
  });
  render();
});

Usage

For small integer quantities — seats, copies, nights. For typing a number directly, use Input type=number instead.

Accessibility

Real buttons with aria-labels; the value is an <output> with aria-live so changes are announced. Buttons disable at the bounds.