webdevpuneet.com

Quantity Stepper — Free HTML CSS JS Snippet

Quantity Stepper · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="page">

  <!-- Basic stepper -->
  <div class="demo-row">
    <span class="demo-label">Basic</span>
    <div class="stepper" id="s1">
      <button class="step-btn" onclick="change('s1',-1)" aria-label="Decrease">−</button>
      <input class="step-val" id="s1-val" type="number" value="1" min="1" max="99" onchange="clamp(this,1,99)" aria-label="Quantity">
      <button class="step-btn" onclick="change('s1',1)" aria-label="Increase">+</button>
    </div>
  </div>

  <!-- With product info -->
  <div class="demo-row">
    <span class="demo-label">Product</span>
    <div class="product-row">
      <div class="product-info">
        <span class="product-name">Wireless Headphones</span>
        <span class="product-price" id="price">$89.00</span>
      </div>
      <div class="stepper compact" id="s2">
        <button class="step-btn" onclick="change('s2',-1)" aria-label="Decrease">−</button>
        <input class="step-val" id="s2-val" type="number" value="1" min="1" max="10" onchange="clamp(this,1,10)" aria-label="Quantity">
        <button class="step-btn" onclick="change('s2',1)" aria-label="Increase">+</button>
      </div>
    </div>
  </div>

  <!-- Pill style -->
  <div class="demo-row">
    <span class="demo-label">Pill</span>
    <div class="stepper pill" id="s3">
      <button class="step-btn" onclick="change('s3',-1)" aria-label="Decrease">−</button>
      <input class="step-val" id="s3-val" type="number" value="3" min="0" max="20" onchange="clamp(this,0,20)" aria-label="Quantity">
      <button class="step-btn" onclick="change('s3',1)" aria-label="Increase">+</button>
    </div>
  </div>

  <!-- Large with stock indicator -->
  <div class="demo-row">
    <span class="demo-label">With stock</span>
    <div class="stepper-wrap">
      <div class="stepper large" id="s4">
        <button class="step-btn" onclick="change('s4',-1)" aria-label="Decrease">−</button>
        <input class="step-val" id="s4-val" type="number" value="1" min="1" max="5" onchange="clamp(this,1,5)" aria-label="Quantity">
        <button class="step-btn" onclick="change('s4',1)" aria-label="Increase">+</button>
      </div>
      <span class="stock-badge" id="stock">5 left in stock</span>
    </div>
  </div>

  <button class="add-btn" onclick="addToCart()">Add to cart</button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Native spin buttons hidden: -webkit-appearance:none + -moz-appearance:textfield
change(id,delta): reads min/max from HTML attributes, applies delta, clamps
clamp(input,min,max): validates direct keyboard input on change event
onUpdate() hook: reactive live price and stock updates per stepper id
Four variants: basic / compact / pill (accent buttons) / large
Stock badge: turns red when remaining count drops to 2 or below
Add to cart feedback: green success state with item count, 2s reset
All variants share same JS — visual differences via CSS classes only

About this UI Snippet

Quantity Stepper — Four Variants, Spin Button Hidden, Min/Max Clamp & Live Price Update

Screenshot of the Quantity Stepper snippet rendered live

A quantity stepper is the essential form control for any e-commerce product page, cart, or booking form where users select a numeric quantity within a range. The native HTML number input with its browser-rendered spin buttons looks inconsistent across devices and cannot be styled to match a design system. This snippet provides four quantity stepper variants — basic, compact product row, pill style, and large with stock indicator — all with hidden native spinners, custom +/- buttons, min/max clamping, and live reactive updates.

Hiding the native spinner

The native up/down arrows on number inputs are hidden via two CSS rules: input::-webkit-outer-spin-button and ::-webkit-inner-spin-button with -webkit-appearance: none (for Chrome/Safari), and -moz-appearance: textfield on the input (for Firefox). This removes all browser-native styling, leaving a clean text-like input that only the custom buttons can increment.

The change() function

change(id, delta) reads the current input value, applies the delta (+1 or -1), clamps between min and max using Math.min(max, Math.max(min, val)), and writes back. Reading min and max from input.min and input.max attributes means the bounds are declared in HTML and read dynamically — no hardcoded values in JavaScript.

The clamp() function

Users can also type directly into the input. onchange fires when the input loses focus or Enter is pressed. clamp(input, min, max) validates and corrects any out-of-bounds value. This prevents users from entering 0, negative numbers, or values above the maximum stock.

Live reactive updates

The onUpdate() hook fires on every change event. In the product variant, it updates the displayed price (quantity × unit price). In the stock variant, it updates the remaining stock badge and changes its colour to red when fewer than 2 items remain — communicating urgency.

Accessibility

Each button has aria-label="Increase" and aria-label="Decrease". The input has aria-label="Quantity". Keyboard users can Tab to the input and type directly, or Tab to the buttons and press Space/Enter. The min/max attributes are also read by assistive technology.

Styling variants

All four variants share the same base .stepper CSS and JS — visual differences come from additional CSS classes (.compact, .pill, .large). This makes it easy to pick one variant and drop it into your project without carrying unused styles.

Connecting to a cart API

In the add() function (the same role as the add-to-cart button), replace the feedback animation with a fetch call: fetch("/api/cart", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ productId: "prod_123", qty: +document.getElementById("s4-val").value }) }). Show success state on resolve and revert to "Add to cart" on reject. The quantity value is always a valid number between min and max due to the clamp() guard.

Step by step

How to Use

  1. 1
    Click + and − buttons to increment or decrement valuesEach stepper has independent min/max bounds. The large stepper (bottom) shows a live stock count that decrements. The product stepper updates the total price with each change.
  2. 2
    Set your min, max, and initial valueUpdate the min, max, and value attributes on each input element. The change() function reads these attributes dynamically — no JavaScript changes needed.
  3. 3
    Choose your visual variantUse class="stepper" for the default, "stepper compact" for small inline contexts, "stepper pill" for a rounded pill with accent-coloured buttons, or "stepper large" for high-visibility product pages.
  4. 4
    Wire the onUpdate hook to your logicAdd your business logic inside onUpdate(id, val). Update a cart subtotal, remaining stock count, days preview, or any other derived value that depends on the quantity.
  5. 5
    Connect to a shopping cart APIIn addToCart(), replace the feedback animation with a POST to your cart API: fetch("/api/cart", { method:"POST", body: JSON.stringify({ productId, qty: +input.value }) }). Show a success state on resolve.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with controlled useState value, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

E-commerce product page quantity selector
The product row variant shows name, live price calculation, and quantity control in one line. The large + stock variant adds urgency cues when stock runs low. Both are standard patterns on Amazon, Shopify, and most e-commerce platforms.
Shopping cart line item quantity adjustment
Use the compact variant inside each cart row. Changing the quantity updates the line total and cart subtotal. Set max to the available stock for each item to prevent over-ordering.
Booking and reservation quantity selection
Use for ticket count, number of guests, number of nights, or number of seats. The pill variant works well for booking interfaces where the stepper sits beside a date picker and proceed button.
Configuration and settings numeric controls
Use for selecting API rate limits, team seat counts, retry attempts, timeout values, or any bounded integer setting in a configuration panel. The basic variant keeps it subtle and minimal.
Study native input spinner removal and custom control pattern
The snippet demonstrates the correct cross-browser technique for removing native number input spinners (-webkit vs -moz) and replacing them with custom buttons. This pattern applies to any design system that needs styled number inputs.
Quantity-based pricing calculators
Wire multiple steppers together: seats × price per seat + storage GB × GB rate. The onUpdate() hook fires on each change, making it straightforward to recompute a total from multiple quantity inputs simultaneously.

Got questions?

Frequently Asked Questions

Two CSS rules are required. For Chrome, Safari, and Edge: input[type=number]::-webkit-outer-spin-button, input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }. For Firefox: input[type=number] { -moz-appearance: textfield; }. Both rules are needed for cross-browser support. Without the -moz rule, Firefox still shows its own spin arrows. After hiding the native arrows, the custom + and - buttons become the only way to increment.

Click "JSX" to download. Replace the input with a controlled React input: <input type="number" value={qty} onChange={e => setQty(Math.min(max, Math.max(min, +e.target.value)))} />. The + button calls setQty(prev => Math.min(max, prev + 1)) and the - button calls setQty(prev => Math.max(min, prev - 1)). Pass min, max, and initial qty as props. Derive dependent values (price, stock) with useMemo from the qty state.

Add dynamic disabled state: the + button has :disabled { opacity: 0.35; cursor: not-allowed; }. In the change() function, after updating the value, update both button states: minBtn.disabled = newVal <= min; maxBtn.disabled = newVal >= max. Query both buttons by their relative position: const btns = stepper.querySelectorAll(".step-btn"); btns[0].disabled = newVal <= min; btns[1].disabled = newVal >= max.

Set the min attribute to the minimum order quantity (e.g., min="5"). In clamp(), if the entered value is below the minimum, show a warning: if (v < minQty) { warningEl.textContent = "Minimum order is " + minQty; warningEl.style.display = "block"; } else { warningEl.style.display = "none"; }. The warning disappears when the value meets the minimum. This pattern is common on wholesale B2B product pages.