1 GitHub star = 1 new component.

Star the repo
Formsinputstepper

Numeric Stepper

Quantity stepper with plus and minus buttons.

Live Preview

Usage

Copy and paste this code into your React component:

tsx
import { StepperInput } from 'react-component-kit-ui/forms';

<StepperInput id="qty" label="Quantity" value={1} min={1} max={10} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/StepperInput.tsx + StepperInput.css:

StepperInput.tsx
'use client';

import { useEffect, useRef } from 'react';
import './StepperInput.css';

/**
 * StepperInput โ€” A numeric input with increment and decrement buttons for precise control.
 * 
 * @param {string} label - Optional input label.
 * @param {string} id - HTML ID for the number input.
 * @param {string} name - HTML name binding.
 * @param {number} value - Initial default value. Default is 1.
 * @param {number} min - Minimum allowed value.
 * @param {number} max - Maximum allowed value.
 * @param {number} step - Numeric increment step. Default is 1.
 */
export interface StepperInputProps {
  label?: string;
  id: string;
  name?: string;
  value?: number;
  min?: number;
  max?: number;
  step?: number;
}

export function StepperInput({ label, id, name, value = 1, min, max, step = 1 }: StepperInputProps) {

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    document.querySelectorAll('[data-stepper]').forEach(wrapper => {
        const input = wrapper.querySelector('[data-step-input]') as HTMLInputElement;
        const minus = wrapper.querySelector('[data-step-minus]');
        const plus = wrapper.querySelector('[data-step-plus]');
        
        if (!input || !minus || !plus) return;
    
        const step = parseFloat(input.step) || 1;
        const min = input.min !== "" ? parseFloat(input.min) : -Infinity;
        const max = input.max !== "" ? parseFloat(input.max) : Infinity;
    
        minus.addEventListener('click', () => {
          const current = parseFloat(input.value) || 0;
          if (current - step >= min) input.value = (current - step).toString();
        });
    
        plus.addEventListener('click', () => {
          const current = parseFloat(input.value) || 0;
          if (current + step <= max) input.value = (current + step).toString();
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <div className="stepper-container" data-rk-dbf2a1="">
        {label && <label className="stepper-label" htmlFor={id} data-rk-dbf2a1="">{label}</label>}
        <div className="stepper" data-stepper data-rk-dbf2a1="">
          <button type="button" className="stepper-btn stepper-btn--minus" data-step-minus aria-label="Decrease value" data-rk-dbf2a1="">โˆ’</button>
          <input type="number" id={id} name={name} min={min} max={max} step={step} defaultValue={value} className="stepper-input" data-step-input data-rk-dbf2a1="" />
          <button type="button" className="stepper-btn stepper-btn--plus" data-step-plus aria-label="Increase value" data-rk-dbf2a1="">+</button>
        </div>
      </div>
    </>
  );
}

export default StepperInput;
StepperInput.css
/* StepperInput โ€” ported from astro-component-kit (forms/StepperInput.astro) */

.stepper-container:where([data-rk-dbf2a1]) { display: flex; flex-direction: column; gap: var(--sp-2, 0.5rem); }
.stepper-label:where([data-rk-dbf2a1]) { font-size: 0.85rem; font-weight: 600; color: var(--c-text-2, #94a3b8); margin-left: 0.5rem; }

.stepper:where([data-rk-dbf2a1]) { 
  display: flex; align-items: center; 
  background: var(--c-bg-elev, rgba(255,255,255,0.05)); 
  border-radius: var(--r-md, 12px); 
  border: 1px solid var(--c-border, rgba(255,255,255,0.1)); 
  overflow: hidden; 
  width: fit-content;
}

.stepper-btn:where([data-rk-dbf2a1]) { 
  width: 40px; height: 40px; 
  background: var(--c-primary, #6366f1); 
  border: none; color: #fff; 
  font-size: 1.25rem; font-weight: 600; 
  cursor: pointer; transition: all 0.2s; 
  display: grid; place-items: center;
}

.stepper-btn:hover:where([data-rk-dbf2a1]) { background: var(--c-primary-light, #818cf8); }
.stepper-btn:active:where([data-rk-dbf2a1]) { transform: scale(0.95); }

.stepper-input:where([data-rk-dbf2a1]) { 
  width: 60px; height: 40px; 
  background: transparent; 
  border: none; text-align: center; 
  color: var(--c-text-1, #fff); 
  font-size: 1rem; font-weight: 700;
  outline: none; -moz-appearance: textfield; 
  font-family: inherit;
}

.stepper-input:where([data-rk-dbf2a1])::-webkit-inner-spin-button,
.stepper-input:where([data-rk-dbf2a1])::-webkit-outer-spin-button { display: none; }

Quick Info

Category
Forms
Filename
StepperInput.tsx
Dependencies
None โ€” pure React + CSS
Tags
inputstepper