Quantity Stepper
A React ± quantity stepper for ecommerce: press-and-hold acceleration, min/max with an at-limit shake, direct typing, a trash icon at zero, and rolling digits, in compact and pill variants.
The −/+ quantity control every cart and product page needs, with the details filled in: one tap steps once, holding the button starts repeating after 0.4s and speeds up after 2s; hitting the min or max shakes the whole control horizontally and disables that side's button; with min set to 0 and allowRemove on, the minus button turns into a trash icon at zero and calls onRemove instead; editable lets users tap the number and type directly (applied and clamped on blur or Enter, Esc cancels, with an IME-composition guard for Chinese and other input methods). Digits roll one column at a time, in the direction of the change; it supports controlled and uncontrolled use, compact and pill variants, and three sizes.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add quantity-stepper.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<QuantityStepper/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/quantity-stepper.en.jsonOr, once registries are configured in components.json, install it as @webberui/quantity-stepper.
Usage
import { QuantityStepper } from "@/components/ui/quantity-stepper";
// Uncontrolled: defaults to min 1, max 99
<QuantityStepper defaultValue={1} onChange={(n) => console.log(n)} />
// Cart row: controlled, trash icon at zero, direct typing, limit of 3 per order
<QuantityStepper
value={qty}
onChange={setQty}
min={0}
max={3}
allowRemove
onRemove={() => removeItem(id)}
editable
variant="pill"
size="sm"
/>
// Show a unit (screen readers read this text too)
<QuantityStepper formatValue={(n) => `${n} Items`} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Controlled value; when omitted the component is uncontrolled |
defaultValue | number | 1 | Initial value in uncontrolled mode, clamped into min–max first |
onChange | (value: number) => void | — | Callback when the value changes; the argument is the clamped new value |
min | number | 1 | Lower bound |
max | number | 99 | Upper bound |
step | number | 1 | Amount per step; with decimals, display and rounding follow its decimal places |
allowRemove | boolean | false | When min is 0 and the value reaches zero, the minus button becomes a trash icon and calls onRemove (instead of being disabled) |
onRemove | () => void | — | Callback when the trash button is clicked; requires allowRemove and min of 0 |
editable | boolean | false | Tap the number to type directly; applied and clamped on blur or Enter, Esc cancels |
variant | "compact" | "pill" | "compact" | Look: compact is a bordered square group, pill is a pill track with round buttons |
size | "sm" | "md" | "lg" | "md" | Size |
disabled | boolean | false | Disable all interaction |
formatValue | (value: number) => string | — | Custom display text for the value (e.g. with a unit); also used for aria-valuetext |
labels | QuantityStepperLabels | see below | aria-labels for the buttons and the value |
className | string | — | Forwarded to the outermost container |
QuantityStepperLabels
| Field | Type | Default | Description |
|---|---|---|---|
decrement | string | "Decrease Quantity" (decrease quantity) | aria-label for the minus button |
increment | string | "Increase Quantity" (increase quantity) | aria-label for the plus button |
remove | string | "Remove" (remove) | aria-label for the trash button |
value | string | "Quantity" (quantity) | aria-label for the value (spinbutton) |
clampQuantity(n, min, max, step?) and the QuantityStepperVariant, QuantityStepperSize, and QuantityStepperLabels types are also named exports, so the same clamp logic can be reused at the form layer.
How it works
- Hold timing:
pointerdownsteps once immediately; after 400ms of holding it steps every 120ms, and 2s after the press it speeds up to every 60ms.pointerup/pointercancelare listened for onwindow, so sliding off the button or the button becoming disabled mid-hold still stops it; repeating stops at the limit, and every timer is cleared on unmount - At-limit shake: only a user action that hits a bound shakes the control (
x: [0, -4, 4, -2, 2, 0]); an initial value that already sits at a bound does not shake on load. In remove mode, reaching zero turns the minus into a trash icon rather than hitting a wall, so it does not shake - Rolling digits: each character is its own column, keyed by its position from the right, so
9 → 10rolls only the ones column while the tens column expands in from the left; on increase the new character enters from below, on decrease from above - Direct typing: full-width digits are converted to ASCII; non-numeric input discards the change; after applying, focus returns to the value element so keyboard users do not fall through to the body
Accessibility
- The value element is
role="spinbutton"witharia-valuenow/aria-valuemin/aria-valuemax, andaria-valuetextreads theformatValueoutput; −/+ are nativebuttons pointing at the value element viaaria-controls - Keyboard: with focus anywhere inside the control, ↑/↓ step once, PageUp/PageDown step by 10×, Home/End jump to the min/max; when
editable, Enter/Space or typing a digit while the value is focused starts editing, and Enter is ignored while an IME (Chinese and others) is composing - Because focus stays on the button when stepping with the mouse, a visually hidden
aria-live="polite"region announces the new value; the button at a limit is disabled with the nativedisabledattribute, and the trash button swaps in its own aria-label - When the user has "reduce motion" enabled at the system level, the shake, rolling digits, and icon-swap animations are disabled and the number simply updates
Star Rating
A classic star rating React component: half stars, hover-preview fill, arrow and number key control, a bounce on select, and a read-only mode that shows the average, review count and a distribution chart; icon, colour and star count are all customisable.
NPS Survey Card
An NPS survey card for React: 0-10 segmented buttons coloured by detractor/passive/promoter band, a follow-up question that expands after scoring, a thank-you state, plus npsCategory and npsScore helpers.